diff --git a/esphome/__main__.py b/esphome/__main__.py index dcd2dddb4b..54c1aa112a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -343,9 +343,10 @@ def upload_program(config, args, host): password = ota_conf.get(CONF_PASSWORD, "") if ( - not is_ip_address(CORE.address) + not is_ip_address(CORE.address) # pylint: disable=too-many-boolean-expressions and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]) and CONF_MQTT in config + and (not args.device or args.device == "MQTT") ): from esphome import mqtt @@ -768,7 +769,9 @@ def parse_args(argv): ) parser_upload = subparsers.add_parser( - "upload", help="Validate the configuration and upload the latest binary." + "upload", + help="Validate the configuration and upload the latest binary.", + parents=[mqtt_options], ) parser_upload.add_argument( "configuration", help="Your YAML configuration file(s).", nargs="+" diff --git a/esphome/automation.py b/esphome/automation.py index 8475858a9c..b25ffa5abe 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -18,10 +18,20 @@ from esphome.util import Registry def maybe_simple_id(*validators): + """Allow a raw ID to be specified in place of a config block. + If the value that's being validated is a dictionary, it's passed as-is to the specified validators. Otherwise, it's + wrapped in a dict that looks like ``{"id": }``, and that dict is then handed off to the specified validators. + """ return maybe_conf(CONF_ID, *validators) def maybe_conf(conf, *validators): + """Allow a raw value to be specified in place of a config block. + If the value that's being validated is a dictionary, it's passed as-is to the specified validators. Otherwise, it's + wrapped in a dict that looks like ``{: }``, and that dict is then handed off to the specified + validators. + (This is a general case of ``maybe_simple_id`` that allows the wrapping key to be something other than ``id``.) + """ validator = cv.All(*validators) @schema_extractor("maybe") diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 17899c31cb..344f2d5a03 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -97,9 +97,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_OPEN_EVT: { - this->response_offset_ = 0; - this->response_length_ = 0; - ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + if (param->open.status == ESP_GATT_OK) { + this->response_offset_ = 0; + this->response_length_ = 0; + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + } break; } case ESP_GATTC_CONNECT_EVT: { diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 008c7768ed..4cc99001ae 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -26,7 +26,9 @@ void Am43::setup() { void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_OPEN_EVT: { - this->logged_in_ = false; + if (param->open.status == ESP_GATT_OK) { + this->logged_in_ = false; + } break; } case ESP_GATTC_DISCONNECT_EVT: { diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 17826ea7ed..b8073abc19 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -47,6 +47,7 @@ service APIConnection { rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} rpc date_command (DateCommandRequest) returns (void) {} rpc time_command (TimeCommandRequest) returns (void) {} + rpc datetime_command (DateTimeCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -1777,3 +1778,40 @@ message ValveCommandRequest { float position = 3; bool stop = 4; } + +// ==================== DATETIME DATETIME ==================== +message ListEntitiesDateTimeResponse { + option (id) = 112; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; +} +message DateTimeStateResponse { + option (id) = 113; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + // If the datetime does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + fixed32 epoch_seconds = 3; +} +message DateTimeCommandRequest { + option (id) = 114; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + fixed32 epoch_seconds = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ec09604d95..b31212bbdb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -772,6 +772,44 @@ void APIConnection::time_command(const TimeCommandRequest &msg) { } #endif +#ifdef USE_DATETIME_DATETIME +bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { + if (!this->state_subscription_) + return false; + + DateTimeStateResponse resp{}; + resp.key = datetime->get_object_id_hash(); + resp.missing_state = !datetime->has_state(); + if (datetime->has_state()) { + ESPTime state = datetime->state_as_esptime(); + resp.epoch_seconds = state.timestamp; + } + return this->send_date_time_state_response(resp); +} +bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { + ListEntitiesDateTimeResponse msg; + msg.key = datetime->get_object_id_hash(); + msg.object_id = datetime->get_object_id(); + if (datetime->has_own_name()) + msg.name = datetime->get_name(); + msg.unique_id = get_default_unique_id("datetime", datetime); + msg.icon = datetime->get_icon(); + msg.disabled_by_default = datetime->is_disabled_by_default(); + msg.entity_category = static_cast(datetime->get_entity_category()); + + return this->send_list_entities_date_time_response(msg); +} +void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { + datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); + if (datetime == nullptr) + return; + + auto call = datetime->make_call(); + call.set_datetime(msg.epoch_seconds); + call.perform(); +} +#endif + #ifdef USE_TEXT bool APIConnection::send_text_state(text::Text *text, std::string state) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2c1d733d3e..ee466c5d10 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection { bool send_time_info(datetime::TimeEntity *time); void time_command(const TimeCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATETIME + bool send_datetime_state(datetime::DateTimeEntity *datetime); + bool send_datetime_info(datetime::DateTimeEntity *datetime); + void datetime_command(const DateTimeCommandRequest &msg) override; +#endif #ifdef USE_TEXT bool send_text_state(text::Text *text, std::string state); bool send_text_info(text::Text *text); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3f01d88c58..6ec1870d72 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -8093,6 +8093,179 @@ void ValveCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + 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(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(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool DateTimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->epoch_seconds = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_fixed32(3, this->epoch_seconds); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateTimeStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" epoch_seconds: "); + sprintf(buffer, "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->epoch_seconds = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_fixed32(2, this->epoch_seconds); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateTimeCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" epoch_seconds: "); + sprintf(buffer, "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9a6aab254d..14fd95df37 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2060,6 +2060,51 @@ class ValveCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class ListEntitiesDateTimeResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateTimeStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + uint32_t epoch_seconds{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateTimeCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + uint32_t epoch_seconds{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index ced81fa643..093fe917e0 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -591,6 +591,24 @@ bool APIServerConnectionBase::send_valve_state_response(const ValveStateResponse #endif #ifdef USE_VALVE #endif +#ifdef USE_DATETIME_DATETIME +bool APIServerConnectionBase::send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_date_time_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(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_(msg, 113); +} +#endif +#ifdef USE_DATETIME_DATETIME +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -1064,6 +1082,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); #endif this->on_valve_command_request(msg); +#endif + break; + } + case 114: { +#ifdef USE_DATETIME_DATETIME + DateTimeCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); +#endif + this->on_date_time_command_request(msg); #endif break; } @@ -1379,6 +1408,19 @@ void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) this->time_command(msg); } #endif +#ifdef USE_DATETIME_DATETIME +void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->datetime_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index c8b2bc5789..196d904aca 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -294,6 +294,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_VALVE virtual void on_valve_command_request(const ValveCommandRequest &value){}; +#endif +#ifdef USE_DATETIME_DATETIME + bool send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg); +#endif +#ifdef USE_DATETIME_DATETIME + bool send_date_time_state_response(const DateTimeStateResponse &msg); +#endif +#ifdef USE_DATETIME_DATETIME + virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -358,6 +367,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_DATETIME_TIME virtual void time_command(const TimeCommandRequest &msg) = 0; #endif +#ifdef USE_DATETIME_DATETIME + virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -453,6 +465,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_DATETIME_TIME void on_time_command_request(const TimeCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATETIME + void on_date_time_command_request(const DateTimeCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 6d4e4db1e8..0725547771 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -273,6 +273,15 @@ void APIServer::on_time_update(datetime::TimeEntity *obj) { } #endif +#ifdef USE_DATETIME_DATETIME +void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_datetime_state(obj); +} +#endif + #ifdef USE_TEXT void APIServer::on_text_update(text::Text *obj, const std::string &state) { if (obj->is_internal()) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e9e03cde0d..2e1fbdf67c 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -72,6 +72,9 @@ class APIServer : public Component, public Controller { #ifdef USE_DATETIME_TIME void on_time_update(datetime::TimeEntity *obj) override; #endif +#ifdef USE_DATETIME_DATETIME + void on_datetime_update(datetime::DateTimeEntity *obj) override; +#endif #ifdef USE_TEXT void on_text_update(text::Text *obj, const std::string &state) override; #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 82bfd45333..a7dbf9a6e7 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -71,6 +71,12 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->cl bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); } #endif +#ifdef USE_DATETIME_DATETIME +bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { + return this->client_->send_datetime_info(datetime); +} +#endif + #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 19cd99ea01..c1fd8b82c4 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -52,6 +52,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_DATETIME_TIME bool on_time(datetime::TimeEntity *time) override; #endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 7aa8e8ffac..005ab0e6da 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -48,6 +48,11 @@ bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->cl #ifdef USE_DATETIME_TIME bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } #endif +#ifdef USE_DATETIME_DATETIME +bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { + return this->client_->send_datetime_state(datetime); +} +#endif #ifdef USE_TEXT bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); } #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 17d444c441..8c725e422e 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -49,6 +49,9 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_DATETIME_TIME bool on_time(datetime::TimeEntity *time) override; #endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index bfec882e07..7ac201b2db 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -51,15 +51,15 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { MultiClickTriggerEvent evt = this->timing_[*this->at_index_]; if (evt.max_length != 4294967294UL) { - ESP_LOGV(TAG, "A i=%u min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT + ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); } else if (*this->at_index_ + 1 != this->timing_.size()) { - ESP_LOGV(TAG, "B i=%u min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT + ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->cancel_timeout("is_not_valid"); this->schedule_is_valid_(evt.min_length); } else { - ESP_LOGV(TAG, "C i=%u min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT + ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->is_valid_ = false; this->cancel_timeout("is_not_valid"); this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 97a25262cb..543752853e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -25,9 +25,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_connections_free(); break; } + case ESP_GATTC_CLOSE_EVT: { + this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); + this->set_address(0); + this->proxy_->send_connections_free(); + break; + } case ESP_GATTC_OPEN_EVT: { - if (param->open.conn_id != this->conn_id_) - break; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); this->set_address(0); @@ -39,9 +43,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = false; break; } - case ESP_GATTC_CFG_MTU_EVT: { - if (param->cfg_mtu.conn_id != this->conn_id_) - break; + case ESP_GATTC_CFG_MTU_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: { if (!this->seen_mtu_or_services_) { // We don't know if we will get the MTU or the services first, so // only send the device connection true if we have already received @@ -53,24 +56,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_connections_free(); break; } - case ESP_GATTC_SEARCH_CMPL_EVT: { - if (param->search_cmpl.conn_id != this->conn_id_) - break; - if (!this->seen_mtu_or_services_) { - // We don't know if we will get the MTU or the services first, so - // only send the device connection true if we have already received - // the mtu. - this->seen_mtu_or_services_ = true; - break; - } - this->proxy_->send_device_connection(this->address_, true, this->mtu_); - this->proxy_->send_connections_free(); - break; - } case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->conn_id_) - break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->read.handle, param->read.status); @@ -89,8 +76,6 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga } case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { - if (param->write.conn_id != this->conn_id_) - break; if (param->write.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->write.handle, param->write.status); @@ -131,8 +116,6 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->conn_id_) - break; ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index 2cc2c512f3..669d40a68d 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -4,6 +4,7 @@ from esphome.components import uart from esphome.const import CONF_ID, CONF_ADDRESS CODEOWNERS = ["@s1lvi0"] +MULTI_CONF = True DEPENDENCIES = ["uart"] CONF_BMS_DALY_ID = "bms_daly_id" diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index a22c60aae9..639a035159 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -1,6 +1,5 @@ import esphome.codegen as cg -# import cpp_generator as cpp import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt, time @@ -13,6 +12,7 @@ from esphome.const import ( CONF_TYPE, CONF_MQTT_ID, CONF_DATE, + CONF_DATETIME, CONF_TIME, CONF_YEAR, CONF_MONTH, @@ -27,6 +27,7 @@ from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@rfdarter", "@jesserockz"] +DEPENDENCIES = ["time"] IS_PLATFORM_COMPONENT = True @@ -34,10 +35,12 @@ datetime_ns = cg.esphome_ns.namespace("datetime") DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase) DateEntity = datetime_ns.class_("DateEntity", DateTimeBase) TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase) +DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase) # Actions DateSetAction = datetime_ns.class_("DateSetAction", automation.Action) TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action) +DateTimeSetAction = datetime_ns.class_("DateTimeSetAction", automation.Action) DateTimeStateTrigger = datetime_ns.class_( "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) @@ -46,6 +49,12 @@ DateTimeStateTrigger = datetime_ns.class_( OnTimeTrigger = datetime_ns.class_( "OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity) ) +OnDateTimeTrigger = datetime_ns.class_( + "OnDateTimeTrigger", + automation.Trigger, + cg.Component, + cg.Parented.template(DateTimeEntity), +) DATETIME_MODES = [ "DATE", @@ -61,45 +70,55 @@ _DATETIME_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), } ), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), } ).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)) def date_schema(class_: MockObjClass) -> cv.Schema: - schema = { - cv.GenerateID(): cv.declare_id(class_), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent), - cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True), - } + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent), + cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True), + } + ) return _DATETIME_SCHEMA.extend(schema) def time_schema(class_: MockObjClass) -> cv.Schema: - schema = { - cv.GenerateID(): cv.declare_id(class_), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent), - cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True), - cv.Inclusive( - CONF_ON_TIME, - group_of_inclusion=CONF_ON_TIME, - msg="`on_time` and `time_id` must both be specified", - ): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger), - } - ), - cv.Inclusive(CONF_TIME_ID, group_of_inclusion=CONF_ON_TIME): cv.use_id( - time.RealTimeClock - ), - } + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent), + cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True), + cv.Optional(CONF_ON_TIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger), + } + ), + } + ) return _DATETIME_SCHEMA.extend(schema) def datetime_schema(class_: MockObjClass) -> cv.Schema: - schema = { - cv.GenerateID(): cv.declare_id(class_), - cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of("DATETIME", upper=True), - } + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTDateTimeComponent + ), + cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of( + "DATETIME", upper=True + ), + cv.Optional(CONF_ON_TIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnDateTimeTrigger), + } + ), + } + ) return _DATETIME_SCHEMA.extend(schema) @@ -113,13 +132,11 @@ async def setup_datetime_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) - rtc_id = config.get(CONF_TIME_ID) - rtc = None - if rtc_id is not None: - rtc = await cg.get_variable(rtc_id) + rtc = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_rtc(rtc)) + for conf in config.get(CONF_ON_TIME, []): - assert rtc is not None - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], rtc) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) await automation.build_automation(trigger, [], conf) await cg.register_component(trigger, conf) await cg.register_parented(trigger, var) @@ -161,16 +178,16 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) - date = config[CONF_DATE] - if cg.is_template(date): - template_ = await cg.templatable(config[CONF_DATE], [], cg.ESPTime) + date_config = config[CONF_DATE] + if cg.is_template(date_config): + template_ = await cg.templatable(date_config, [], cg.ESPTime) cg.add(action_var.set_date(template_)) else: date_struct = cg.StructInitializer( cg.ESPTime, - ("day_of_month", date[CONF_DAY]), - ("month", date[CONF_MONTH]), - ("year", date[CONF_YEAR]), + ("day_of_month", date_config[CONF_DAY]), + ("month", date_config[CONF_MONTH]), + ("year", date_config[CONF_YEAR]), ) cg.add(action_var.set_date(date_struct)) return action_var @@ -194,7 +211,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): time_config = config[CONF_TIME] if cg.is_template(time_config): - template_ = await cg.templatable(config[CONF_TIME], [], cg.ESPTime) + template_ = await cg.templatable(time_config, [], cg.ESPTime) cg.add(action_var.set_time(template_)) else: time_struct = cg.StructInitializer( @@ -205,3 +222,35 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ) cg.add(action_var.set_time(time_struct)) return action_var + + +@automation.register_action( + "datetime.datetime.set", + DateTimeSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(DateTimeEntity), + cv.Required(CONF_DATETIME): cv.Any(cv.returning_lambda, cv.date_time()), + }, + ), +) +async def datetime_datetime_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + datetime_config = config[CONF_DATETIME] + if cg.is_template(datetime_config): + template_ = await cg.templatable(datetime_config, [], cg.ESPTime) + cg.add(action_var.set_datetime(template_)) + else: + datetime_struct = cg.StructInitializer( + cg.ESPTime, + ("second", datetime_config[CONF_SECOND]), + ("minute", datetime_config[CONF_MINUTE]), + ("hour", datetime_config[CONF_HOUR]), + ("day_of_month", datetime_config[CONF_DAY]), + ("month", datetime_config[CONF_MONTH]), + ("year", datetime_config[CONF_YEAR]), + ) + cg.add(action_var.set_datetime(datetime_struct)) + return action_var diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 8b58a8faf7..19399c1e59 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -40,10 +40,13 @@ void DateCall::validate_() { if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { ESP_LOGE(TAG, "Year must be between 1970 and 3000"); this->year_.reset(); + this->month_.reset(); + this->day_.reset(); } if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { ESP_LOGE(TAG, "Month must be between 1 and 12"); this->month_.reset(); + this->day_.reset(); } if (this->day_.has_value()) { uint16_t year = 0; diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 2f2d27e102..c8240390e3 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -5,6 +5,8 @@ #include "esphome/core/entity_base.h" #include "esphome/core/time.h" +#include "esphome/components/time/real_time_clock.h" + namespace esphome { namespace datetime { @@ -17,9 +19,14 @@ class DateTimeBase : public EntityBase { void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; } + time::RealTimeClock *get_rtc() const { return this->rtc_; } + protected: CallbackManager state_callback_; + time::RealTimeClock *rtc_; + bool has_state_{false}; }; diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp new file mode 100644 index 0000000000..9a61d341e4 --- /dev/null +++ b/esphome/components/datetime/datetime_entity.cpp @@ -0,0 +1,252 @@ +#include "datetime_entity.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace datetime { + +static const char *const TAG = "datetime.datetime_entity"; + +void DateTimeEntity::publish_state() { + if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { + this->has_state_ = false; + return; + } + if (this->year_ < 1970 || this->year_ > 3000) { + this->has_state_ = false; + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + return; + } + if (this->month_ < 1 || this->month_ > 12) { + this->has_state_ = false; + ESP_LOGE(TAG, "Month must be between 1 and 12"); + return; + } + if (this->day_ > days_in_month(this->month_, this->year_)) { + this->has_state_ = false; + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); + return; + } + if (this->hour_ > 23) { + this->has_state_ = false; + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + return; + } + if (this->minute_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + return; + } + if (this->second_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Second must be between 0 and 59"); + return; + } + this->has_state_ = true; + ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, + this->month_, this->day_, this->hour_, this->minute_, this->second_); + this->state_callback_.call(); +} + +DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } + +ESPTime DateTimeEntity::state_as_esptime() const { + ESPTime obj; + obj.year = this->year_; + obj.month = this->month_; + obj.day_of_month = this->day_; + obj.hour = this->hour_; + obj.minute = this->minute_; + obj.second = this->second_; + obj.day_of_week = 1; // Required to be valid for recalc_timestamp_local but not used. + obj.day_of_year = 1; // Required to be valid for recalc_timestamp_local but not used. + obj.recalc_timestamp_local(false); + return obj; +} + +void DateTimeCall::validate_() { + if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + this->year_.reset(); + this->month_.reset(); + this->day_.reset(); + } + if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { + ESP_LOGE(TAG, "Month must be between 1 and 12"); + this->month_.reset(); + this->day_.reset(); + } + if (this->day_.has_value()) { + uint16_t year = 0; + uint8_t month = 0; + if (this->month_.has_value()) { + month = *this->month_; + } else { + if (this->parent_->month != 0) { + month = this->parent_->month; + } else { + ESP_LOGE(TAG, "Month must be set to validate day"); + this->day_.reset(); + } + } + if (this->year_.has_value()) { + year = *this->year_; + } else { + if (this->parent_->year != 0) { + year = this->parent_->year; + } else { + ESP_LOGE(TAG, "Year must be set to validate day"); + this->day_.reset(); + } + } + if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) { + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month); + this->day_.reset(); + } + } + + if (this->hour_.has_value() && this->hour_ > 23) { + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + this->hour_.reset(); + } + if (this->minute_.has_value() && this->minute_ > 59) { + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + this->minute_.reset(); + } + if (this->second_.has_value() && this->second_ > 59) { + ESP_LOGE(TAG, "Second must be between 0 and 59"); + this->second_.reset(); + } +} + +void DateTimeCall::perform() { + this->validate_(); + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + + if (this->year_.has_value()) { + ESP_LOGD(TAG, " Year: %d", *this->year_); + } + if (this->month_.has_value()) { + ESP_LOGD(TAG, " Month: %d", *this->month_); + } + if (this->day_.has_value()) { + ESP_LOGD(TAG, " Day: %d", *this->day_); + } + if (this->hour_.has_value()) { + ESP_LOGD(TAG, " Hour: %d", *this->hour_); + } + if (this->minute_.has_value()) { + ESP_LOGD(TAG, " Minute: %d", *this->minute_); + } + if (this->second_.has_value()) { + ESP_LOGD(TAG, " Second: %d", *this->second_); + } + this->parent_->control(*this); +} + +DateTimeCall &DateTimeCall::set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, + uint8_t second) { + this->year_ = year; + this->month_ = month; + this->day_ = day; + this->hour_ = hour; + this->minute_ = minute; + this->second_ = second; + return *this; +}; + +DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) { + return this->set_datetime(datetime.year, datetime.month, datetime.day_of_month, datetime.hour, datetime.minute, + datetime.second); +}; + +DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) { + ESPTime val{}; + if (!ESPTime::strptime(datetime, val)) { + ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); + return *this; + } + return this->set_datetime(val); +} + +DateTimeCall &DateTimeCall::set_datetime(time_t epoch_seconds) { + ESPTime val = ESPTime::from_epoch_local(epoch_seconds); + return this->set_datetime(val); +} + +DateTimeCall DateTimeEntityRestoreState::to_call(DateTimeEntity *datetime) { + DateTimeCall call = datetime->make_call(); + call.set_datetime(this->year, this->month, this->day, this->hour, this->minute, this->second); + return call; +} + +void DateTimeEntityRestoreState::apply(DateTimeEntity *time) { + time->year_ = this->year; + time->month_ = this->month; + time->day_ = this->day; + time->hour_ = this->hour; + time->minute_ = this->minute; + time->second_ = this->second; + time->publish_state(); +} + +static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider + // there has been a drastic time synchronization + +void OnDateTimeTrigger::loop() { + if (!this->parent_->has_state()) { + return; + } + ESPTime time = this->parent_->rtc_->now(); + if (!time.is_valid()) { + return; + } + if (this->last_check_.has_value()) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { + // We went back in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped back!"); + } else if (*this->last_check_ >= time) { + // already handled this one + return; + } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + // We went ahead in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped ahead!"); + this->last_check_ = time; + return; + } + + while (true) { + this->last_check_->increment_second(); + if (*this->last_check_ >= time) + break; + + if (this->matches_(*this->last_check_)) { + this->trigger(); + break; + } + } + } + + this->last_check_ = time; + if (!time.fields_in_range()) { + ESP_LOGW(TAG, "Time is out of range!"); + ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u Day=%02u Month=%02u Year=%04u", time.second, time.minute, + time.hour, time.day_of_month, time.month, time.year); + } + + if (this->matches_(time)) + this->trigger(); +} + +bool OnDateTimeTrigger::matches_(const ESPTime &time) const { + return time.is_valid() && time.year == this->parent_->year && time.month == this->parent_->month && + time.day_of_month == this->parent_->day && time.hour == this->parent_->hour && + time.minute == this->parent_->minute && time.second == this->parent_->second; +} + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h new file mode 100644 index 0000000000..d541fa96b1 --- /dev/null +++ b/esphome/components/datetime/datetime_entity.h @@ -0,0 +1,150 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/time.h" + +#include "datetime_base.h" + +namespace esphome { +namespace datetime { + +#define LOG_DATETIME_DATETIME(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class DateTimeCall; +class DateTimeEntity; + +struct DateTimeEntityRestoreState { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + + DateTimeCall to_call(DateTimeEntity *datetime); + void apply(DateTimeEntity *datetime); +} __attribute__((packed)); + +class DateTimeEntity : public DateTimeBase { + protected: + uint16_t year_; + uint8_t month_; + uint8_t day_; + uint8_t hour_; + uint8_t minute_; + uint8_t second_; + + public: + void publish_state(); + DateTimeCall make_call(); + + ESPTime state_as_esptime() const override; + + const uint16_t &year = year_; + const uint8_t &month = month_; + const uint8_t &day = day_; + const uint8_t &hour = hour_; + const uint8_t &minute = minute_; + const uint8_t &second = second_; + + protected: + friend class DateTimeCall; + friend struct DateTimeEntityRestoreState; + friend class OnDateTimeTrigger; + + virtual void control(const DateTimeCall &call) = 0; +}; + +class DateTimeCall { + public: + explicit DateTimeCall(DateTimeEntity *parent) : parent_(parent) {} + void perform(); + DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); + DateTimeCall &set_datetime(ESPTime datetime); + DateTimeCall &set_datetime(const std::string &datetime); + DateTimeCall &set_datetime(time_t epoch_seconds); + + DateTimeCall &set_year(uint16_t year) { + this->year_ = year; + return *this; + } + DateTimeCall &set_month(uint8_t month) { + this->month_ = month; + return *this; + } + DateTimeCall &set_day(uint8_t day) { + this->day_ = day; + return *this; + } + DateTimeCall &set_hour(uint8_t hour) { + this->hour_ = hour; + return *this; + } + DateTimeCall &set_minute(uint8_t minute) { + this->minute_ = minute; + return *this; + } + DateTimeCall &set_second(uint8_t second) { + this->second_ = second; + return *this; + } + + optional get_year() const { return this->year_; } + optional get_month() const { return this->month_; } + optional get_day() const { return this->day_; } + optional get_hour() const { return this->hour_; } + optional get_minute() const { return this->minute_; } + optional get_second() const { return this->second_; } + + protected: + void validate_(); + + DateTimeEntity *parent_; + + optional year_; + optional month_; + optional day_; + optional hour_; + optional minute_; + optional second_; +}; + +template class DateTimeSetAction : public Action, public Parented { + 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 { + public: + void loop() override; + + protected: + bool matches_(const ESPTime &time) const; + + optional last_check_; +}; + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATETIME diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 98558152d7..ea5e6684d0 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -94,8 +94,6 @@ void TimeEntityRestoreState::apply(TimeEntity *time) { time->publish_state(); } -#ifdef USE_TIME - static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider // there has been a drastic time synchronization @@ -103,7 +101,7 @@ void OnTimeTrigger::loop() { if (!this->parent_->has_state()) { return; } - ESPTime time = this->rtc_->now(); + ESPTime time = this->parent_->rtc_->now(); if (!time.is_valid()) { return; } @@ -148,8 +146,6 @@ bool OnTimeTrigger::matches_(const ESPTime &time) const { time.second == this->parent_->second; } -#endif - } // namespace datetime } // namespace esphome diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 956c09e2b4..62e593d28a 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -10,10 +10,6 @@ #include "datetime_base.h" -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace datetime { @@ -27,6 +23,7 @@ namespace datetime { class TimeCall; class TimeEntity; +class OnTimeTrigger; struct TimeEntityRestoreState { uint8_t hour; @@ -62,6 +59,7 @@ class TimeEntity : public DateTimeBase { protected: friend class TimeCall; friend struct TimeEntityRestoreState; + friend class OnTimeTrigger; virtual void control(const TimeCall &call) = 0; }; @@ -115,22 +113,16 @@ template class TimeSetAction : public Action, public Pare } }; -#ifdef USE_TIME - class OnTimeTrigger : public Trigger<>, public Component, public Parented { public: - explicit OnTimeTrigger(time::RealTimeClock *rtc) : rtc_(rtc) {} void loop() override; protected: bool matches_(const ESPTime &time) const; - time::RealTimeClock *rtc_; optional last_check_; }; -#endif - } // namespace datetime } // namespace esphome diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 992799008a..c4bb12b75d 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -38,6 +38,7 @@ DisplayOnPageChangeTrigger = display_ns.class_( ) CONF_ON_PAGE_CHANGE = "on_page_change" +CONF_SHOW_TEST_CARD = "show_test_card" DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -82,6 +83,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( } ), cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) @@ -113,6 +115,8 @@ async def setup_display_core_(var, config): await automation.build_automation( trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf ) + if config.get(CONF_SHOW_TEST_CARD): + cg.add(var.show_test_card()) async def register_display(var, config): diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 010e6eca0b..75205292f7 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -1,7 +1,7 @@ #include "display.h" - +#include "display_color_utils.h" #include - +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -507,7 +507,9 @@ void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); } - if (this->page_ != nullptr) { + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { this->page_->get_writer()(*this); } else if (this->writer_.has_value()) { (*this->writer_)(*this); @@ -608,6 +610,62 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } +const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' + +void Display::test_card() { + int w = get_width(), h = get_height(), image_w, image_h; + this->clear(); + this->show_test_card_ = false; + if (this->get_display_type() == DISPLAY_TYPE_COLOR) { + Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255); + image_w = std::min(w - 20, 310); + image_h = std::min(h - 20, 255); + + int shift_x = (w - image_w) / 2; + int shift_y = (h - image_h) / 2; + int line_w = (image_w - 6) / 6; + int image_c = image_w / 2; + for (auto i = 0; i <= image_h; i++) { + int c = esp_scale(i, image_h); + this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c)); + this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); // + + this->horizontal_line(shift_x + image_c - line_w, shift_y + i, line_w, g.fade_to_white(c)); + this->horizontal_line(shift_x + image_c, shift_y + i, line_w, g.fade_to_black(c)); + + this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c)); + this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c)); + } + this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); + + uint16_t shift_r = shift_x + line_w - (8 * 3); + uint16_t shift_g = shift_x + image_c - (8 * 3); + uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); + shift_y = h / 2 - (8 * 3); + for (auto i = 0; i < 8; i++) { + uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); + uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); + uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); + for (auto k = 0; k < 8; k++) { + if ((ftr & (1 << k)) != 0) { + this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftg & (1 << k)) != 0) { + this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftb & (1 << k)) != 0) { + this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + } + } + } + this->rectangle(0, 0, w, h, Color(127, 0, 127)); + this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255)); + this->stop_poller(); +} + DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show_next() { this->next_->show(); } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index a30ba976b4..4ee7ef93cb 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -631,6 +631,9 @@ class Display : public PollingComponent { */ bool clip(int x, int y); + void test_card(); + void show_test_card() { this->show_test_card_ = true; } + protected: bool clamp_x_(int x, int w, int &min_x, int &max_x); bool clamp_y_(int y, int h, int &min_y, int &max_y); @@ -659,6 +662,7 @@ class Display : public PollingComponent { std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; std::vector clipping_rectangle_; + bool show_test_card_{false}; }; class DisplayPage { diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8ae6262f2f..5d74838daa 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -32,6 +32,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, __version__, + CONF_PLATFORM_VERSION, ) from esphome.core import CORE, HexInt, TimePeriod import esphome.config_validation as cv @@ -365,8 +366,6 @@ def final_validate(config): return config -CONF_PLATFORM_VERSION = "platform_version" - ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index ae83715aea..98e7792792 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -142,7 +142,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), param->open.status); this->set_state(espbt::ClientState::IDLE); - return false; + break; } auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); if (ret) { diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 00729921a3..64b127bda3 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP8266, + CONF_PLATFORM_VERSION, ) from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv @@ -146,7 +147,6 @@ def _parse_platform_version(value): return value -CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 789f121cf3..241e884386 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -25,8 +25,8 @@ IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ DEVICE_CLASS_BUTTON, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_DOORBELL, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ] diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 5a18429789..3b62b8ca66 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -129,7 +129,13 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo uint8_t bitmask = 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_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { 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) { display->draw_pixel_at(glyph_x, glyph_y, color); } else if (pixel != 0) { - float on = (float) pixel / bpp_max; - float off = 1.0 - on; - Color blended; - blended.r = color.r * on + background.r * off; - blended.g = color.r * on + background.g * off; - blended.b = color.r * on + background.b * off; + auto on = (float) pixel / (float) bpp_max; + auto blended = + Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b)); display->draw_pixel_at(glyph_x, glyph_y, blended); } } diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 0e437a3425..09f7557714 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -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); for (auto *trace : traces_) { 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 has_prev = false; bool prev_b = false; @@ -177,22 +177,26 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo bool b = (trace->get_line_type() & bit) == bit; if (b) { int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; + auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { + if (y >= y_offset && y < y_offset + this->height_) + buff->draw_pixel_at(x, y, c); + }; if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { - for (uint16_t t = 0; t < thick; t++) { - buff->draw_pixel_at(x, y + t, c); + for (int16_t t = 0; t < thick; t++) { + draw_pixel_at(x, y + t); } } else { int16_t mid_y = (y + prev_y + thick) / 2; if (y > prev_y) { - for (uint16_t t = prev_y + thick; t <= mid_y; t++) - buff->draw_pixel_at(x + 1, t, c); - for (uint16_t t = mid_y + 1; t < y + thick; t++) - buff->draw_pixel_at(x, t, c); + for (int16_t t = prev_y + thick; t <= mid_y; t++) + draw_pixel_at(x + 1, t); + for (int16_t t = mid_y + 1; t < y + thick; t++) + draw_pixel_at(x, t); } else { - for (uint16_t t = prev_y - 1; t >= mid_y; t--) - buff->draw_pixel_at(x + 1, t, c); - for (uint16_t t = mid_y - 1; t >= y; t--) - buff->draw_pixel_at(x, t, c); + for (int16_t t = prev_y - 1; t >= mid_y; t--) + draw_pixel_at(x + 1, t); + for (int16_t t = mid_y - 1; t >= y; t--) + draw_pixel_at(x, t); } } prev_y = y; diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index fcbad41388..4a4e519009 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -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) { - int total_height = 0; + int16_t total_height = 0; + int16_t max_width = 0; int y_padding = 2; bool scroll_menu_items = false; std::vector menu_dimensions; @@ -118,6 +119,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const menu_dimensions.push_back(item_dimensions); total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); + max_width = std::max(max_width, item_dimensions.w); if (total_height <= bounds->h) { 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 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++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; @@ -176,7 +179,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const dimensions.x = bounds->x; this->draw_item(display, item, &dimensions, selected); - y_offset = dimensions.y + dimensions.h + y_padding; + y_offset += dimensions.h + y_padding; } 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 = 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(); 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(), - ~foreground_color); + background_color); } void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 6c830f9bad..c1b47826a2 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,6 +1,7 @@ #pragma once #include "abstract_aqi_calculator.h" +// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf namespace esphome { namespace hm3301 { @@ -15,14 +16,16 @@ class AQICalculator : public AbstractAQICalculator { } protected: - static const int AMOUNT_OF_LEVELS = 6; + static const int AMOUNT_OF_LEVELS = 7; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, + {201, 300}, {301, 400}, {401, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, + {151, 250}, {251, 350}, {351, 500}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, - {255, 354}, {355, 424}, {425, 604}}; + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, + {355, 424}, {425, 504}, {505, 604}}; int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int grid_index = get_grid_index_(value, array); diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 602d537bcb..1475df0975 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -61,28 +61,57 @@ void I2SAudioMicrophone::start_() { .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, }; + esp_err_t err; + #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); - i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + + err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + err = i2s_adc_enable(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } - i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); - i2s_adc_enable(this->parent_->get_port()); } else #endif { if (this->pdm_) config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); - i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } i2s_pin_config_t pin_config = this->parent_->get_pin_config(); pin_config.data_in_num = this->din_pin_; - i2s_set_pin(this->parent_->get_port(), &pin_config); + err = i2s_set_pin(this->parent_->get_port(), &pin_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } } this->state_ = microphone::STATE_RUNNING; this->high_freq_.start(); + this->status_clear_error(); } void I2SAudioMicrophone::stop() { @@ -96,11 +125,33 @@ void I2SAudioMicrophone::stop() { } void I2SAudioMicrophone::stop_() { - i2s_stop(this->parent_->get_port()); - i2s_driver_uninstall(this->parent_->get_port()); + esp_err_t err; +#if SOC_I2S_SUPPORTS_ADC + if (this->adc_) { + err = i2s_adc_disable(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + } +#endif + err = i2s_stop(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + err = i2s_driver_uninstall(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error uninstalling I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } this->parent_->unlock(); this->state_ = microphone::STATE_STOPPED; this->high_freq_.stop(); + this->status_clear_error(); } size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index c62ca43ca1..f7829a3f44 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -150,6 +150,7 @@ class AutomationLightEffect : public LightEffect { struct StrobeLightEffectColor { LightColorValues color; uint32_t duration; + uint32_t transition_length; }; class StrobeLightEffect : public LightEffect { @@ -174,7 +175,7 @@ class StrobeLightEffect : public LightEffect { } call.set_publish(false); call.set_save(false); - call.set_transition_length_if_supported(0); + call.set_transition_length_if_supported(this->colors_[this->at_color_].transition_length); call.perform(); this->last_switch_ = now; } diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 5093ace949..be50f63321 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -266,6 +266,9 @@ async def random_effect_to_code(config, effect_id): cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, } ), cv.has_at_least_one_key( @@ -310,6 +313,7 @@ async def strobe_effect_to_code(config, effect_id): ), ), ("duration", color[CONF_DURATION]), + ("transition_length", color[CONF_TRANSITION_LENGTH]), ) ) cg.add(var.set_colors(colors)) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 7a42140ef6..064362c619 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -115,6 +115,7 @@ MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent) MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent) +MQTTDateTimeComponent = mqtt_ns.class_("MQTTDateTimeComponent", MQTTComponent) MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp new file mode 100644 index 0000000000..4fa44aafb8 --- /dev/null +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -0,0 +1,84 @@ +#include "mqtt_datetime.h" + +#include +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_TIME + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.datetime.time"; + +using namespace esphome::datetime; + +MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {} + +void MQTTDateTimeComponent::setup() { + this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { + auto call = this->datetime_->make_call(); + if (root.containsKey("year")) { + call.set_year(root["year"]); + } + if (root.containsKey("month")) { + call.set_month(root["month"]); + } + if (root.containsKey("day")) { + call.set_day(root["day"]); + } + if (root.containsKey("hour")) { + call.set_hour(root["hour"]); + } + if (root.containsKey("minute")) { + call.set_minute(root["minute"]); + } + if (root.containsKey("second")) { + call.set_second(root["second"]); + } + call.perform(); + }); + this->datetime_->add_on_state_callback([this]() { + this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, this->datetime_->hour, + this->datetime_->minute, this->datetime_->second); + }); +} + +void MQTTDateTimeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTDateTimeComponent::component_type() const { return "datetime"; } +const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; } + +void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // Nothing extra to add here +} +bool MQTTDateTimeComponent::send_initial_state() { + if (this->datetime_->has_state()) { + return this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, + this->datetime_->hour, this->datetime_->minute, this->datetime_->second); + } else { + return true; + } +} +bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, + uint8_t second) { + return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { + root["year"] = year; + root["month"] = month; + root["day"] = day; + root["hour"] = hour; + root["minute"] = minute; + root["second"] = second; + }); +} + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_TIME +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h new file mode 100644 index 0000000000..f0d68ad2e1 --- /dev/null +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_TIME + +#include "esphome/components/datetime/datetime_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTDateTimeComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTDateTimeComponent instance with the provided friendly_name and time + * + * @param time The time entity. + */ + explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + datetime::DateTimeEntity *datetime_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index d192e62430..1856a023cc 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -24,8 +24,10 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_OPEN_EVT: - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); - this->delayed_disconnect_(); + if (param->open.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + this->delayed_disconnect_(); + } break; case ESP_GATTC_DISCONNECT_EVT: ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 4946ad1b77..8541b41ff7 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -77,8 +77,17 @@ void QMC5883LComponent::dump_config() { float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void QMC5883LComponent::update() { uint8_t status = false; - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) - this->read_byte(QMC5883L_REGISTER_STATUS, &status); + this->read_byte(QMC5883L_REGISTER_STATUS, &status); + + // Always request X,Y,Z regardless if there are sensors for them + // to avoid https://github.com/esphome/issues/issues/5731 + uint16_t raw_x, raw_y, raw_z; + if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) { + this->status_set_warning(); + return; + } float mg_per_bit; switch (this->range_) { @@ -93,36 +102,11 @@ void QMC5883LComponent::update() { } // in µT - float x = NAN, y = NAN, z = NAN; - if (this->x_sensor_ != nullptr || this->heading_sensor_ != nullptr) { - uint16_t raw_x; - if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x)) { - this->status_set_warning(); - return; - } - x = int16_t(raw_x) * mg_per_bit * 0.1f; - } - if (this->y_sensor_ != nullptr || this->heading_sensor_ != nullptr) { - uint16_t raw_y; - if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y)) { - this->status_set_warning(); - return; - } - y = int16_t(raw_y) * mg_per_bit * 0.1f; - } - if (this->z_sensor_ != nullptr) { - uint16_t raw_z; - if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) { - this->status_set_warning(); - return; - } - z = int16_t(raw_z) * mg_per_bit * 0.1f; - } + const float x = int16_t(raw_x) * mg_per_bit * 0.1f; + const float y = int16_t(raw_y) * mg_per_bit * 0.1f; + const float z = int16_t(raw_z) * mg_per_bit * 0.1f; - float heading = NAN; - if (this->heading_sensor_ != nullptr) { - heading = atan2f(0.0f - x, y) * 180.0f / M_PI; - } + float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; float temp = NAN; if (this->temperature_sensor_ != nullptr) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b262a068fb..ace455add7 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_RP2040, + CONF_PLATFORM_VERSION, ) from esphome.core import CORE, coroutine_with_priority, EsphomeError from esphome.helpers import mkdir_p, write_file, copy_file_if_changed @@ -125,8 +126,6 @@ def _parse_platform_version(value): return value -CONF_PLATFORM_VERSION = "platform_version" - ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ef368015b1..2d3a79ccae 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -116,7 +116,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return - substitutions = config[CONF_SUBSTITUTIONS] + substitutions = config.get(CONF_SUBSTITUTIONS) if substitutions is None: substitutions = command_line_substitutions elif command_line_substitutions: diff --git a/esphome/components/template/datetime/__init__.py b/esphome/components/template/datetime/__init__.py index 53d9d1b9d3..bf7154ef76 100644 --- a/esphome/components/template/datetime/__init__.py +++ b/esphome/components/template/datetime/__init__.py @@ -31,6 +31,10 @@ TemplateTime = template_ns.class_( "TemplateTime", datetime.TimeEntity, cg.PollingComponent ) +TemplateDateTime = template_ns.class_( + "TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent +) + def validate(config): config = config.copy() @@ -78,6 +82,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_date=False), } ), + "DATETIME": datetime.datetime_schema(TemplateDateTime) + .extend(_BASE_SCHEMA) + .extend( + { + cv.Optional(CONF_INITIAL_VALUE): cv.date_time(), + } + ), }, upper=True, ), @@ -116,6 +127,17 @@ async def to_code(config): ("hour", initial_value[CONF_HOUR]), ) cg.add(var.set_initial_value(time_struct)) + elif config[CONF_TYPE] == "DATETIME": + datetime_struct = cg.StructInitializer( + cg.ESPTime, + ("second", initial_value[CONF_SECOND]), + ("minute", initial_value[CONF_MINUTE]), + ("hour", initial_value[CONF_HOUR]), + ("day_of_month", initial_value[CONF_DAY]), + ("month", initial_value[CONF_MONTH]), + ("year", initial_value[CONF_YEAR]), + ) + cg.add(var.set_initial_value(datetime_struct)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp new file mode 100644 index 0000000000..3ab74e197f --- /dev/null +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -0,0 +1,150 @@ +#include "template_datetime.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.datetime"; + +void TemplateDateTime::setup() { + if (this->f_.has_value()) + return; + + ESPTime state{}; + + if (!this->restore_value_) { + state = this->initial_value_; + } else { + datetime::DateTimeEntityRestoreState temp; + this->pref_ = global_preferences->make_preference(194434090U ^ + this->get_object_id_hash()); + if (this->pref_.load(&temp)) { + temp.apply(this); + return; + } else { + // set to inital value if loading from pref failed + state = this->initial_value_; + } + } + + this->year_ = state.year; + this->month_ = state.month; + this->day_ = state.day_of_month; + this->hour_ = state.hour; + this->minute_ = state.minute; + this->second_ = state.second; + this->publish_state(); +} + +void TemplateDateTime::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); +} + +void TemplateDateTime::control(const datetime::DateTimeCall &call) { + bool has_year = call.get_year().has_value(); + bool has_month = call.get_month().has_value(); + bool has_day = call.get_day().has_value(); + bool has_hour = call.get_hour().has_value(); + bool has_minute = call.get_minute().has_value(); + bool has_second = call.get_second().has_value(); + + ESPTime value = {}; + if (has_year) + value.year = *call.get_year(); + + if (has_month) + value.month = *call.get_month(); + + if (has_day) + value.day_of_month = *call.get_day(); + + if (has_hour) + value.hour = *call.get_hour(); + + if (has_minute) + value.minute = *call.get_minute(); + + if (has_second) + value.second = *call.get_second(); + + this->set_trigger_->trigger(value); + + if (this->optimistic_) { + if (has_year) + this->year_ = *call.get_year(); + if (has_month) + this->month_ = *call.get_month(); + if (has_day) + this->day_ = *call.get_day(); + if (has_hour) + this->hour_ = *call.get_hour(); + if (has_minute) + this->minute_ = *call.get_minute(); + if (has_second) + this->second_ = *call.get_second(); + this->publish_state(); + } + + if (this->restore_value_) { + datetime::DateTimeEntityRestoreState temp = {}; + if (has_year) { + temp.year = *call.get_year(); + } else { + temp.year = this->year_; + } + if (has_month) { + temp.month = *call.get_month(); + } else { + temp.month = this->month_; + } + if (has_day) { + temp.day = *call.get_day(); + } else { + temp.day = this->day_; + } + if (has_hour) { + temp.hour = *call.get_hour(); + } else { + temp.hour = this->hour_; + } + if (has_minute) { + temp.minute = *call.get_minute(); + } else { + temp.minute = this->minute_; + } + if (has_second) { + temp.second = *call.get_second(); + } else { + temp.second = this->second_; + } + + this->pref_.save(&temp); + } +} + +void TemplateDateTime::dump_config() { + LOG_DATETIME_DATETIME("", "Template DateTime", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATETIME diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h new file mode 100644 index 0000000000..cb1fd01132 --- /dev/null +++ b/esphome/components/template/datetime/template_datetime.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_TIME + +#include "esphome/components/datetime/datetime_entity.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace template_ { + +class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { + public: + void set_template(std::function()> &&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 *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 *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 9b903d098b..2b9a95c6bd 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -13,6 +13,8 @@ #endif #include +#include + namespace esphome { namespace time { diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index b7a826d237..42cf66c2ab 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -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_manual_control(bool value) { this->manual_control_ = value; } void set_assumed_state(bool value) { this->assumed_state_ = value; } + cover::CoverOperation get_last_operation() const { return this->last_operation_; } protected: void control(const cover::CoverCall &call) override; diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index dc43cbf5a7..bba60efc0a 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -49,6 +49,9 @@ WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InV2R2", WaveshareEPaper ) GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) +WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InDKE", WaveshareEPaper +) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) @@ -115,6 +118,7 @@ MODELS = { "2.90in-b": ("b", WaveshareEPaper2P9InB), "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), + "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index cf43c4cc32..7224aa44ed 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1127,6 +1127,131 @@ void WaveshareEPaper2P9InB::dump_config() { LOG_UPDATE_INTERVAL(this); } +// DKE 2.9 +// https://www.badge.team/docs/badges/sha2017/hardware/#e-ink-display-the-dke-group-depg0290b1 +// https://www.badge.team/docs/badges/sha2017/hardware/DEPG0290B01V3.0.pdf +static const uint8_t LUT_SIZE_DKE = 70; +static const uint8_t UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0xA0, 0x90, 0x50, 0x0, 0x0, 0x0, 0x0, 0x50, 0x90, 0xA0, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x90, 0x50, 0x0, + 0x0, 0x0, 0x0, 0x50, 0x90, 0xA0, 0x0, 0x0, 0x0, 0x0, 0x00, 0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0xF, + 0xF, 0x0, 0x0, 0x0, 0xF, 0xF, 0x0, 0x0, 0x02, 0xF, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, +}; +static const uint8_t PART_UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x50, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x05, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t FULL_UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0x90, 0x50, 0xa0, 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00, 0x90, 0x50, 0xa0, 0x50, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, + 0x04, 0x00, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x0e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +void WaveshareEPaper2P9InDKE::initialize() { + // Hardware reset + delay(10); + this->reset_pin_->digital_write(false); + delayMicroseconds(200); + this->reset_pin_->digital_write(true); + delayMicroseconds(200); + // Wait for busy low + this->wait_until_idle_(); + // Software reset + this->command(0x12); + // Wait for busy low + this->wait_until_idle_(); + // Set Analog Block Control + this->command(0x74); + this->data(0x54); + // Set Digital Block Control + this->command(0x7E); + this->data(0x3B); + // Set display size and driver output control + this->command(0x01); + // this->data(0x27); + // this->data(0x01); + // this->data(0x00); + this->data(this->get_height_internal() - 1); + this->data((this->get_height_internal() - 1) >> 8); + this->data(0x00); // ? GD = 0, SM = 0, TB = 0 + // Ram data entry mode + this->command(0x11); + this->data(0x03); + // Set Ram X address + this->command(0x44); + this->data(0x00); + this->data(0x0F); + // Set Ram Y address + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data(0x27); + this->data(0x01); + // Set border + this->command(0x3C); + // this->data(0x80); + this->data(0x01); + // Set VCOM value + this->command(0x2C); + this->data(0x26); + // Gate voltage setting + this->command(0x03); + this->data(0x17); + // Source voltage setting + this->command(0x04); + this->data(0x41); + this->data(0x00); + this->data(0x32); + // Frame setting 50hz + this->command(0x3A); + this->data(0x30); + this->command(0x3B); + this->data(0x0A); + // Load LUT + this->command(0x32); + for (uint8_t v : FULL_UPDATE_LUT_DKE) + this->data(v); +} + +void HOT WaveshareEPaper2P9InDKE::display() { + ESP_LOGI(TAG, "Performing e-paper update."); + // Set Ram X address counter + this->command(0x4e); + this->data(0); + // Set Ram Y address counter + this->command(0x4f); + this->data(0); + this->data(0); + // Load image (128/8*296) + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + // Image update + this->command(0x22); + this->data(0xC7); + this->command(0x20); + // Wait for busy low + this->wait_until_idle_(); + // Enter deep sleep mode + this->command(0x10); + this->data(0x01); +} +int WaveshareEPaper2P9InDKE::get_width_internal() { return 128; } +int WaveshareEPaper2P9InDKE::get_height_internal() { return 296; } +void WaveshareEPaper2P9InDKE::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in DKE"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} +void WaveshareEPaper2P9InDKE::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + // ======================================================== // 2.90in Type B (LUT from OTP) // Datasheet: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index ffc099ca56..3c4470c30c 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -373,6 +373,30 @@ class WaveshareEPaper2P9InV2R2 : public WaveshareEPaper { void reset_(); }; +class WaveshareEPaper2P9InDKE : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + void set_full_update_every(uint32_t full_update_every); + + protected: + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 8d08783c8c..42af72e872 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -129,6 +129,15 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { } #endif +#ifdef USE_DATETIME_DATETIME +bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->datetime_json(datetime, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *text) { if (this->web_server_->events_.count() == 0) diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index af84cb1d2b..47d427d9b5 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -47,6 +47,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_DATETIME_TIME bool on_time(datetime::TimeEntity *time) override; #endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0202038ffc..6a7b4121f0 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -926,6 +926,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { + if (this->events_.count() == 0) + return; this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -970,6 +972,55 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con } #endif // USE_DATETIME_TIME +#ifdef USE_DATETIME_DATETIME +void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->datetime_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_datetimes()) { + if (obj->get_object_id() != match.id) + continue; + if (request->method() == HTTP_GET && match.method.empty()) { + std::string data = this->datetime_json(obj, DETAIL_STATE); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (!request->hasParam("value")) { + request->send(409); + return; + } + + if (request->hasParam("value")) { + std::string value = request->getParam("value")->value().c_str(); + call.set_datetime(value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); + std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, + obj->minute, obj->second); + root["value"] = value; + root["state"] = value; + }); +} +#endif // USE_DATETIME_DATETIME + #ifdef USE_TEXT void WebServer::on_text_update(text::Text *obj, const std::string &state) { if (this->events_.count() == 0) @@ -1458,6 +1509,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_DATETIME_DATETIME + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime") + return true; +#endif + #ifdef USE_TEXT if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") return true; @@ -1595,6 +1651,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif +#ifdef USE_DATETIME_DATETIME + if (match.domain == "datetime") { + this->handle_datetime_request(request, match); + return; + } +#endif + #ifdef USE_TEXT if (match.domain == "text") { this->handle_text_request(request, match); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 5e8f3f8236..dda14a7e05 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -239,6 +239,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); #endif +#ifdef USE_DATETIME_DATETIME + void on_datetime_update(datetime::DateTimeEntity *obj) override; + /// Handle a datetime request under '/datetime/'. + void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the datetime state with its value as a JSON string. + std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config); +#endif + #ifdef USE_TEXT void on_text_update(text::Text *obj, const std::string &state) override; /// Handle a text input request under '/text/'. diff --git a/esphome/config.py b/esphome/config.py index 1f6867eb59..36a81f677b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -374,7 +374,10 @@ class LoadValidationStep(ConfigValidationStep): path + [CONF_ID], ) continue - result.add_str_error("No platform specified! See 'platform' key.", path) + result.add_str_error( + f"'{self.domain}' requires a 'platform' key but it was not specified.", + path, + ) continue # Remove temp output path and construct new one result.remove_output_path(path, p_domain) @@ -449,9 +452,28 @@ class MetadataValidationStep(ConfigValidationStep): success = True for dependency in self.comp.dependencies: - if dependency not in result: + dependency_parts = dependency.split(".") + if len(dependency_parts) > 2: result.add_str_error( - f"Component {self.domain} requires component {dependency}", + "Dependencies must be specified as a single component or in component.platform format only", + self.path, + ) + return + component_dep = dependency_parts[0] + platform_dep = dependency_parts[-1] + if component_dep not in result: + result.add_str_error( + f"Component {self.domain} requires component {component_dep}", + self.path, + ) + success = False + elif component_dep != platform_dep and ( + not isinstance(platform_list := result.get(component_dep), list) + or not any(CONF_PLATFORM in p for p in platform_list) + or not any(p[CONF_PLATFORM] == platform_dep for p in platform_list) + ): + result.add_str_error( + f"Component {self.domain} requires 'platform: {platform_dep}' in component '{component_dep}'", self.path, ) success = False @@ -756,11 +778,11 @@ def validate_config( CORE.raw_config = config # 1. Load substitutions - if CONF_SUBSTITUTIONS in config: + if CONF_SUBSTITUTIONS in config or command_line_substitutions: from esphome.components import substitutions result[CONF_SUBSTITUTIONS] = { - **config[CONF_SUBSTITUTIONS], + **config.get(CONF_SUBSTITUTIONS, {}), **command_line_substitutions, } result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index bc58979f33..198b44f38d 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -304,7 +304,7 @@ def string(value): """Validate that a configuration value is a string. If not, automatically converts to a string. Note that this can be lossy, for example the input value 60.00 (float) will be turned into - "60.0" (string). For values where this could be a problem `string_string` has to be used. + "60.0" (string). For values where this could be a problem `string_strict` has to be used. """ check_not_templatable(value) if isinstance(value, (dict, list)): diff --git a/esphome/const.py b/esphome/const.py index dfb7fe464c..324b32e847 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -179,6 +179,7 @@ CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" CONF_DATA_TEMPLATE = "data_template" CONF_DATE = "date" +CONF_DATETIME = "datetime" CONF_DAY = "day" CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_WEEK = "days_of_week" @@ -597,6 +598,7 @@ CONF_PIN_D = "pin_d" CONF_PINS = "pins" CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" +CONF_PLATFORM_VERSION = "platform_version" CONF_PLATFORMIO_OPTIONS = "platformio_options" CONF_PM_0_3UM = "pm_0_3um" CONF_PM_0_5UM = "pm_0_5um" diff --git a/esphome/core/application.h b/esphome/core/application.h index 35df350ec3..7487780412 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -45,6 +45,9 @@ #ifdef USE_DATETIME_TIME #include "esphome/components/datetime/time_entity.h" #endif +#ifdef USE_DATETIME_DATETIME +#include "esphome/components/datetime/datetime_entity.h" +#endif #ifdef USE_TEXT #include "esphome/components/text/text.h" #endif @@ -141,6 +144,10 @@ class Application { void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); } #endif +#ifdef USE_DATETIME_DATETIME + void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); } +#endif + #ifdef USE_TEXT void register_text(text::Text *text) { this->texts_.push_back(text); } #endif @@ -335,6 +342,15 @@ class Application { return nullptr; } #endif +#ifdef USE_DATETIME_DATETIME + const std::vector &get_datetimes() { return this->datetimes_; } + datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->datetimes_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif #ifdef USE_TEXT const std::vector &get_texts() { return this->texts_; } text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { @@ -456,6 +472,9 @@ class Application { #ifdef USE_DATETIME_TIME std::vector times_{}; #endif +#ifdef USE_DATETIME_DATETIME + std::vector datetimes_{}; +#endif #ifdef USE_SELECT std::vector selects_{}; #endif diff --git a/esphome/core/component.h b/esphome/core/component.h index 4f244e5fcb..e6ffe96d1e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -85,7 +85,7 @@ class Component { /** priority of setup(). higher -> executed earlier * - * Defaults to 0. + * Defaults to setup_priority::DATA, i.e. 600. * * @return The setup priority of this component */ diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 687f1f6e23..9b02bf527b 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -232,6 +232,21 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_DATETIME_DATETIME + case IteratorState::DATETIME_DATETIME: + if (this->at_ >= App.get_datetimes().size()) { + advance_platform = true; + } else { + auto *datetime = App.get_datetimes()[this->at_]; + if (datetime->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_datetime(datetime); + } + } + break; +#endif #ifdef USE_TEXT case IteratorState::TEXT: if (this->at_ >= App.get_texts().size()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 8f0398cbb3..2b847bc088 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -63,6 +63,9 @@ class ComponentIterator { #ifdef USE_DATETIME_TIME virtual bool on_time(datetime::TimeEntity *time) = 0; #endif +#ifdef USE_DATETIME_DATETIME + virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 0; +#endif #ifdef USE_TEXT virtual bool on_text(text::Text *text) = 0; #endif @@ -132,6 +135,9 @@ class ComponentIterator { #ifdef USE_DATETIME_TIME DATETIME_TIME, #endif +#ifdef USE_DATETIME_DATETIME + DATETIME_DATETIME, +#endif #ifdef USE_TEXT TEXT, #endif diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index eb975eaf6f..0957329500 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -71,6 +71,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); } #endif +#ifdef USE_DATETIME_DATETIME + for (auto *obj : App.get_datetimes()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); + } +#endif #ifdef USE_TEXT for (auto *obj : App.get_texts()) { if (include_internal || !obj->is_internal()) diff --git a/esphome/core/controller.h b/esphome/core/controller.h index da9dbc00a6..e1bf93193a 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -37,6 +37,9 @@ #ifdef USE_DATETIME_TIME #include "esphome/components/datetime/time_entity.h" #endif +#ifdef USE_DATETIME_DATETIME +#include "esphome/components/datetime/datetime_entity.h" +#endif #ifdef USE_TEXT #include "esphome/components/text/text.h" #endif @@ -97,6 +100,9 @@ class Controller { #ifdef USE_DATETIME_TIME virtual void on_time_update(datetime::TimeEntity *obj){}; #endif +#ifdef USE_DATETIME_DATETIME + virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; +#endif #ifdef USE_TEXT virtual void on_text_update(text::Text *obj, const std::string &state){}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index fed73098d2..619a956071 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -39,6 +39,7 @@ #define USE_DATETIME #define USE_DATETIME_DATE #define USE_DATETIME_TIME +#define USE_DATETIME_DATETIME #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 4368576301..fdc0eed774 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -437,7 +437,7 @@ static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } -std::string base64_encode(const char *buf, unsigned int buf_len) { +std::string base64_encode(const uint8_t *buf, size_t buf_len) { std::string ret; int i = 0; int j = 0; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 0004fc7e8e..add671701f 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -178,6 +178,15 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { this->timestamp = res; } +void ESPTime::recalc_timestamp_local(bool use_day_of_year) { + this->recalc_timestamp_utc(use_day_of_year); + this->timestamp -= ESPTime::timezone_offset(); + ESPTime temp = ESPTime::from_epoch_local(this->timestamp); + if (temp.is_dst) { + this->timestamp -= 3600; + } +} + int32_t ESPTime::timezone_offset() { int32_t offset = 0; time_t now = ::time(nullptr); diff --git a/esphome/core/time.h b/esphome/core/time.h index 4300cf26b7..bce1108d93 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -99,6 +99,9 @@ struct ESPTime { /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). void recalc_timestamp_utc(bool use_day_of_year = true); + /// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields. + void recalc_timestamp_local(bool use_day_of_year = true); + /// Convert this ESPTime instance back to a tm struct. struct tm to_c_tm(); diff --git a/script/ci-custom.py b/script/ci-custom.py index 27fcd480f5..abe004dba3 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -623,6 +623,7 @@ def lint_trailing_whitespace(fname, match): "esphome/components/cover/cover.h", "esphome/components/datetime/date_entity.h", "esphome/components/datetime/time_entity.h", + "esphome/components/datetime/datetime_entity.h", "esphome/components/display/display.h", "esphome/components/event/event.h", "esphome/components/fan/fan.h", diff --git a/script/list-components.py b/script/list-components.py index 8e2d47c6b3..5b5fa5811f 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -69,7 +69,9 @@ def create_components_graph(): sys.exit(1) for dependency in comp.dependencies: - add_item_to_components_graph(components_graph, dependency, name) + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) for target_config in TARGET_CONFIGURATIONS: CORE.data[KEY_CORE] = target_config @@ -87,7 +89,9 @@ def create_components_graph(): add_item_to_components_graph(components_graph, platform_name, name) for dependency in platform.dependencies: - add_item_to_components_graph(components_graph, dependency, name) + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) for target_config in TARGET_CONFIGURATIONS: CORE.data[KEY_CORE] = target_config diff --git a/script/sync-device_class.py b/script/sync-device_class.py index 8f91b97997..12e1bb6a9f 100755 --- a/script/sync-device_class.py +++ b/script/sync-device_class.py @@ -6,9 +6,11 @@ import re from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.event import EventDeviceClass from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.components.valve import ValveDeviceClass # pylint: enable=import-error @@ -21,9 +23,11 @@ DOMAINS = { "binary_sensor": BinarySensorDeviceClass, "button": ButtonDeviceClass, "cover": CoverDeviceClass, + "event": EventDeviceClass, "number": NumberDeviceClass, "sensor": SensorDeviceClass, "switch": SwitchDeviceClass, + "valve": ValveDeviceClass, } diff --git a/script/test_build_components b/script/test_build_components index 2396353198..4d91256572 100755 --- a/script/test_build_components +++ b/script/test_build_components @@ -37,9 +37,9 @@ start_esphome() { # Start esphome process echo "> [$target_component] [$test_name] [$target_platform]" - echo "esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file" + echo "esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file" # TODO: Validate escape of Command line substitution value - esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file + esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file } # Find all test yaml files. diff --git a/tests/components/datetime/test.all.yaml b/tests/components/datetime/test.all.yaml index 3f5996bb8b..4e26b68121 100644 --- a/tests/components/datetime/test.all.yaml +++ b/tests/components/datetime/test.all.yaml @@ -1 +1,3 @@ datetime: + +time: diff --git a/tests/components/host/test.host.yaml b/tests/components/host/test.host.yaml new file mode 100644 index 0000000000..3d14c190a6 --- /dev/null +++ b/tests/components/host/test.host.yaml @@ -0,0 +1,15 @@ +time: + - platform: sntp + id: esptime + timezone: Australia/Sydney + +logger: + level: VERBOSE + logs: + lvgl: INFO + display: DEBUG + sensor: INFO + vnc: DEBUG + +host: + mac_address: "62:23:45:AF:B3:DD" diff --git a/tests/components/template/test.all.yaml b/tests/components/template/common.yaml similarity index 88% rename from tests/components/template/test.all.yaml rename to tests/components/template/common.yaml index 64faec36c2..ba7167157b 100644 --- a/tests/components/template/test.all.yaml +++ b/tests/components/template/common.yaml @@ -183,3 +183,25 @@ datetime: - x.hour - x.minute - x.second + - platform: template + name: DateTime + id: test_datetime + type: datetime + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d" + args: + - x.year + - x.month + - x.day_of_month + - x.hour + - x.minute + - x.second + +time: + - platform: sntp # Required for datetime + +wifi: # Required for sntp time + ap: diff --git a/tests/components/template/test.bk72xx.yaml b/tests/components/template/test.bk72xx.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.bk72xx.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3.yaml b/tests/components/template/test.esp32-c3.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-c3.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-idf.yaml b/tests/components/template/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32.yaml b/tests/components/template/test.esp32.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp8266.yaml b/tests/components/template/test.esp8266.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp8266.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.rp2040.yaml b/tests/components/template/test.rp2040.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.rp2040.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32.yaml b/tests/components/waveshare_epaper/test.esp32.yaml index cc6c665e7d..2f06c5c51b 100644 --- a/tests/components/waveshare_epaper/test.esp32.yaml +++ b/tests/components/waveshare_epaper/test.esp32.yaml @@ -83,6 +83,24 @@ display: full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.90in-dke + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper model: 2.70in-b spi_id: spi_id_1 diff --git a/tests/test_build_components/build_components_base.bk72xx.yaml b/tests/test_build_components/build_components_base.bk72xx.yaml index d74cc651eb..9fd9431826 100644 --- a/tests/test_build_components/build_components_base.bk72xx.yaml +++ b/tests/test_build_components/build_components_base.bk72xx.yaml @@ -12,8 +12,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-ard.yaml b/tests/test_build_components/build_components_base.esp32-ard.yaml index e345adcafc..31b7067acc 100644 --- a/tests/test_build_components/build_components_base.esp32-ard.yaml +++ b/tests/test_build_components/build_components_base.esp32-ard.yaml @@ -14,8 +14,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-c3-ard.yaml b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml index 136ee62d4b..8aad447693 100644 --- a/tests/test_build_components/build_components_base.esp32-c3-ard.yaml +++ b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml @@ -14,8 +14,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml index 4e809dd7ce..18584497f4 100644 --- a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml @@ -14,8 +14,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-idf.yaml b/tests/test_build_components/build_components_base.esp32-idf.yaml index 3c4532a958..a62a995e68 100644 --- a/tests/test_build_components/build_components_base.esp32-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-idf.yaml @@ -14,8 +14,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-s2-ard.yaml b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml index db62cdff6a..b8f2639127 100644 --- a/tests/test_build_components/build_components_base.esp32-s2-ard.yaml +++ b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml @@ -15,8 +15,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-s2-idf.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml index 19fb657d8b..62f0f4f7bc 100644 --- a/tests/test_build_components/build_components_base.esp32-s2-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml @@ -15,8 +15,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-s3-ard.yaml b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml index 39e12de08c..25cad038b6 100644 --- a/tests/test_build_components/build_components_base.esp32-s3-ard.yaml +++ b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml @@ -15,8 +15,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml index 95c73f917e..b1d08fcdf8 100644 --- a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -15,8 +15,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.esp8266.yaml b/tests/test_build_components/build_components_base.esp8266.yaml index 0019298ef0..ecf9acd2ba 100644 --- a/tests/test_build_components/build_components_base.esp8266.yaml +++ b/tests/test_build_components/build_components_base.esp8266.yaml @@ -12,8 +12,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.host.yaml b/tests/test_build_components/build_components_base.host.yaml index ec7570d99a..5492cfddd2 100644 --- a/tests/test_build_components/build_components_base.host.yaml +++ b/tests/test_build_components/build_components_base.host.yaml @@ -12,8 +12,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name" diff --git a/tests/test_build_components/build_components_base.rp2040.yaml b/tests/test_build_components/build_components_base.rp2040.yaml index cb1cb6359b..335642374b 100644 --- a/tests/test_build_components/build_components_base.rp2040.yaml +++ b/tests/test_build_components/build_components_base.rp2040.yaml @@ -15,8 +15,4 @@ packages: component_under_test: !include file: $component_test_file vars: - component_name: $component_name - test_name: $test_name - target_platform: $target_platform component_test_file: $component_test_file - component_dir: "../../components/$component_name"