diff --git a/CODEOWNERS b/CODEOWNERS index 90574ca9ba..bbb39c26ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -172,6 +172,7 @@ esphome/components/host/time/* @clydebarrow esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hte501/* @Stock-M esphome/components/http_request/ota/* @oarcher +esphome/components/http_request/update/* @jesserockz esphome/components/htu31d/* @betterengineering esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hyt271/* @Philippe12 @@ -410,6 +411,7 @@ esphome/components/uart/button/* @ssieb esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter +esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/valve/* @esphome/core esphome/components/vbus/* @ssieb diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0becec2348..812a1d74ae 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -48,6 +48,7 @@ service APIConnection { rpc date_command (DateCommandRequest) returns (void) {} rpc time_command (TimeCommandRequest) returns (void) {} rpc datetime_command (DateTimeCommandRequest) returns (void) {} + rpc update_command (UpdateCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -1837,3 +1838,46 @@ message DateTimeCommandRequest { fixed32 key = 1; fixed32 epoch_seconds = 2; } + +// ==================== UPDATE ==================== +message ListEntitiesUpdateResponse { + option (id) = 116; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + + 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; + string device_class = 8; +} +message UpdateStateResponse { + option (id) = 117; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + bool missing_state = 2; + bool in_progress = 3; + bool has_progress = 4; + float progress = 5; + string current_version = 6; + string latest_version = 7; + string title = 8; + string release_summary = 9; + string release_url = 10; +} +message UpdateCommandRequest { + option (id) = 118; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + bool install = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 253f04aa39..2e73a8336e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1287,6 +1287,51 @@ bool APIConnection::send_event_info(event::Event *event) { } #endif +#ifdef USE_UPDATE +bool APIConnection::send_update_state(update::UpdateEntity *update) { + if (!this->state_subscription_) + return false; + + UpdateStateResponse resp{}; + resp.key = update->get_object_id_hash(); + resp.missing_state = !update->has_state(); + if (update->has_state()) { + resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING; + if (update->update_info.has_progress) { + resp.has_progress = true; + resp.progress = update->update_info.progress; + } + resp.current_version = update->update_info.current_version; + resp.latest_version = update->update_info.latest_version; + resp.title = update->update_info.title; + resp.release_summary = update->update_info.summary; + resp.release_url = update->update_info.release_url; + } + + return this->send_update_state_response(resp); +} +bool APIConnection::send_update_info(update::UpdateEntity *update) { + ListEntitiesUpdateResponse msg; + msg.key = update->get_object_id_hash(); + msg.object_id = update->get_object_id(); + if (update->has_own_name()) + msg.name = update->get_name(); + msg.unique_id = get_default_unique_id("update", update); + msg.icon = update->get_icon(); + msg.disabled_by_default = update->is_disabled_by_default(); + msg.entity_category = static_cast(update->get_entity_category()); + msg.device_class = update->get_device_class(); + return this->send_list_entities_update_response(msg); +} +void APIConnection::update_command(const UpdateCommandRequest &msg) { + update::UpdateEntity *update = App.get_update_by_key(msg.key); + if (update == nullptr) + return; + + update->perform(); +} +#endif + bool APIConnection::send_log_message(int level, const char *tag, const char *line) { if (this->log_subscription_ < level) return false; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 293da17fa4..714e806470 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -164,6 +164,12 @@ class APIConnection : public APIServerConnection { bool send_event_info(event::Event *event); #endif +#ifdef USE_UPDATE + bool send_update_state(update::UpdateEntity *update); + bool send_update_info(update::UpdateEntity *update); + void update_command(const UpdateCommandRequest &msg) override; +#endif + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9db6482c49..e6e905c6d1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -8376,6 +8376,262 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesUpdateResponse::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 ListEntitiesUpdateResponse::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; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesUpdateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesUpdateResponse::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); + buffer.encode_string(8, this->device_class); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesUpdateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesUpdateResponse {\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(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + case 3: { + this->in_progress = value.as_bool(); + return true; + } + case 4: { + this->has_progress = value.as_bool(); + return true; + } + default: + return false; + } +} +bool UpdateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 6: { + this->current_version = value.as_string(); + return true; + } + case 7: { + this->latest_version = value.as_string(); + return true; + } + case 8: { + this->title = value.as_string(); + return true; + } + case 9: { + this->release_summary = value.as_string(); + return true; + } + case 10: { + this->release_url = value.as_string(); + return true; + } + default: + return false; + } +} +bool UpdateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->progress = value.as_float(); + return true; + } + default: + return false; + } +} +void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_bool(3, this->in_progress); + buffer.encode_bool(4, this->has_progress); + buffer.encode_float(5, this->progress); + buffer.encode_string(6, this->current_version); + buffer.encode_string(7, this->latest_version); + buffer.encode_string(8, this->title); + buffer.encode_string(9, this->release_summary); + buffer.encode_string(10, this->release_url); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void UpdateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateStateResponse {\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(" in_progress: "); + out.append(YESNO(this->in_progress)); + out.append("\n"); + + out.append(" has_progress: "); + out.append(YESNO(this->has_progress)); + out.append("\n"); + + out.append(" progress: "); + sprintf(buffer, "%g", this->progress); + out.append(buffer); + out.append("\n"); + + out.append(" current_version: "); + out.append("'").append(this->current_version).append("'"); + out.append("\n"); + + out.append(" latest_version: "); + out.append("'").append(this->latest_version).append("'"); + out.append("\n"); + + out.append(" title: "); + out.append("'").append(this->title).append("'"); + out.append("\n"); + + out.append(" release_summary: "); + out.append("'").append(this->release_summary).append("'"); + out.append("\n"); + + out.append(" release_url: "); + out.append("'").append(this->release_url).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->install = value.as_bool(); + return true; + } + default: + return false; + } +} +bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->install); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void UpdateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" install: "); + out.append(YESNO(this->install)); + 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 54cbd20559..ef051eecf1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2130,6 +2130,61 @@ class DateTimeCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class ListEntitiesUpdateResponse : 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{}; + std::string device_class{}; + 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 UpdateStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + bool in_progress{false}; + bool has_progress{false}; + float progress{0.0f}; + std::string current_version{}; + std::string latest_version{}; + std::string title{}; + std::string release_summary{}; + std::string release_url{}; + 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 UpdateCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + bool install{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 7c95bb03ad..269a755e9e 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -611,6 +611,24 @@ bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateR #endif #ifdef USE_DATETIME_DATETIME #endif +#ifdef USE_UPDATE +bool APIServerConnectionBase::send_list_entities_update_response(const ListEntitiesUpdateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_update_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 116); +} +#endif +#ifdef USE_UPDATE +bool APIServerConnectionBase::send_update_state_response(const UpdateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_update_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 117); +} +#endif +#ifdef USE_UPDATE +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -1106,6 +1124,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_timer_event_response(msg); +#endif + break; + } + case 118: { +#ifdef USE_UPDATE + UpdateCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); +#endif + this->on_update_command_request(msg); #endif break; } @@ -1434,6 +1463,19 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ this->datetime_command(msg); } #endif +#ifdef USE_UPDATE +void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->update_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 2f8a2b3def..83bfc2ed98 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -306,6 +306,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_DATETIME_DATETIME virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; +#endif +#ifdef USE_UPDATE + bool send_list_entities_update_response(const ListEntitiesUpdateResponse &msg); +#endif +#ifdef USE_UPDATE + bool send_update_state_response(const UpdateStateResponse &msg); +#endif +#ifdef USE_UPDATE + virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -373,6 +382,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_DATETIME_DATETIME virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; #endif +#ifdef USE_UPDATE + virtual void update_command(const UpdateCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -471,6 +483,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_DATETIME_DATETIME void on_date_time_command_request(const DateTimeCommandRequest &msg) override; #endif +#ifdef USE_UPDATE + void on_update_command_request(const UpdateCommandRequest &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 0725547771..a61ae89243 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -334,6 +334,13 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) { } #endif +#ifdef USE_UPDATE +void APIServer::on_update(update::UpdateEntity *obj) { + for (auto &c : this->clients_) + c->send_update_state(obj); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2e1fbdf67c..43bc8a7348 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -102,6 +102,9 @@ class APIServer : public Component, public Controller { #ifdef USE_EVENT void on_event(event::Event *obj, const std::string &event_type) override; #endif +#ifdef USE_UPDATE + void on_update(update::UpdateEntity *obj) override; +#endif bool is_connected() const; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index a7dbf9a6e7..5fa360d170 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -98,6 +98,9 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); } #endif +#ifdef USE_UPDATE +bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); } +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index c1fd8b82c4..a37586de0f 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -75,6 +75,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_EVENT bool on_event(event::Event *event) override; +#endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 005ab0e6da..5861b1f465 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -77,6 +77,9 @@ bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); } #endif +#ifdef USE_UPDATE +bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 8c725e422e..67c4346210 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -72,6 +72,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; +#endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index a41f552baf..dcc783ea47 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -15,6 +15,8 @@ namespace esphome { namespace http_request { +static const char *const TAG = "http_request.ota"; + void OtaHttpRequestComponent::setup() { #ifdef USE_OTA_STATE_CALLBACK ota::register_ota_platform(this); diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 91c7085517..6a86b4ab43 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -14,7 +14,6 @@ namespace esphome { namespace http_request { -static const char *const TAG = "http_request.ota"; static const uint8_t MD5_SIZE = 32; enum OtaHttpRequestError : uint8_t { diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py new file mode 100644 index 0000000000..356afa1432 --- /dev/null +++ b/esphome/components/http_request/update/__init__.py @@ -0,0 +1,44 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import update +from esphome.const import ( + CONF_SOURCE, +) + +from .. import http_request_ns, CONF_HTTP_REQUEST_ID, HttpRequestComponent +from ..ota import OtaHttpRequestComponent + + +AUTO_LOAD = ["json"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["ota.http_request"] + +HttpRequestUpdate = http_request_ns.class_( + "HttpRequestUpdate", update.UpdateEntity, cg.PollingComponent +) + +CONF_OTA_ID = "ota_id" + +CONFIG_SCHEMA = update.UPDATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HttpRequestUpdate), + cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + cv.Required(CONF_SOURCE): cv.url, + } +).extend(cv.polling_component_schema("6h")) + + +async def to_code(config): + var = await update.new_update(config) + ota_parent = await cg.get_variable(config[CONF_OTA_ID]) + cg.add(var.set_ota_parent(ota_parent)) + request_parent = await cg.get_variable(config[CONF_HTTP_REQUEST_ID]) + cg.add(var.set_request_parent(request_parent)) + + cg.add(var.set_source_url(config[CONF_SOURCE])) + + cg.add_define("USE_OTA_STATE_CALLBACK") + + await cg.register_component(var, config) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp new file mode 100644 index 0000000000..98129e59dc --- /dev/null +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -0,0 +1,157 @@ +#include "http_request_update.h" + +#include "esphome/core/application.h" +#include "esphome/core/version.h" + +#include "esphome/components/json/json_util.h" +#include "esphome/components/network/util.h" + +namespace esphome { +namespace http_request { + +static const char *const TAG = "http_request.update"; + +static const size_t MAX_READ_SIZE = 256; + +void HttpRequestUpdate::setup() { + this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) { + if (state == ota::OTAState::OTA_IN_PROGRESS) { + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = true; + this->update_info_.progress = progress; + this->publish_state(); + } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + this->status_set_error("Failed to install firmware"); + this->publish_state(); + } + }); +} + +void HttpRequestUpdate::update() { + auto container = this->request_parent_->get(this->source_url_); + + if (container == nullptr) { + std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str()); + this->status_set_error(msg.c_str()); + return; + } + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); + this->status_set_error(msg.c_str()); + container->end(); + return; + } + + size_t read_index = 0; + while (container->get_bytes_read() < container->content_length) { + int read_bytes = container->read(data + read_index, MAX_READ_SIZE); + + App.feed_wdt(); + yield(); + + read_index += read_bytes; + } + + std::string response((char *) data, read_index); + allocator.deallocate(data, container->content_length); + + container->end(); + + bool valid = json::parse_json(response, [this](JsonObject root) -> bool { + if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this->update_info_.title = root["name"].as(); + this->update_info_.latest_version = root["version"].as(); + + for (auto build : root["builds"].as()) { + if (!build.containsKey("chipFamily")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + if (build["chipFamily"] == ESPHOME_VARIANT) { + if (!build.containsKey("ota")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + auto ota = build["ota"]; + if (!ota.containsKey("path") || !ota.containsKey("md5")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this->update_info_.firmware_url = ota["path"].as(); + this->update_info_.md5 = ota["md5"].as(); + + if (ota.containsKey("summary")) + this->update_info_.summary = ota["summary"].as(); + if (ota.containsKey("release_url")) + this->update_info_.release_url = ota["release_url"].as(); + + return true; + } + } + return false; + }); + + if (!valid) { + std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); + this->status_set_error(msg.c_str()); + return; + } + + // Merge source_url_ and this->update_info_.firmware_url + if (this->update_info_.firmware_url.find("http") == std::string::npos) { + std::string path = this->update_info_.firmware_url; + if (path[0] == '/') { + std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); + this->update_info_.firmware_url = domain + path; + } else { + std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); + this->update_info_.firmware_url = domain + path; + } + } + + std::string current_version = this->current_version_; + if (current_version.empty()) { +#ifdef ESPHOME_PROJECT_VERSION + current_version = ESPHOME_PROJECT_VERSION; +#else + current_version = ESPHOME_VERSION; +#endif + } + this->update_info_.current_version = current_version; + + if (this->update_info_.latest_version.empty()) { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } else if (this->update_info_.latest_version != this->current_version_) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } + + this->update_info_.has_progress = false; + this->update_info_.progress = 0.0f; + + this->status_clear_error(); + this->publish_state(); +} + +void HttpRequestUpdate::perform() { + if (this->state_ != update::UPDATE_STATE_AVAILABLE) { + return; + } + + this->state_ = update::UPDATE_STATE_INSTALLING; + this->publish_state(); + + this->ota_parent_->set_md5(this->update_info.md5); + this->ota_parent_->set_url(this->update_info.firmware_url); + // Flash in the next loop + this->defer([this]() { this->ota_parent_->flash(); }); +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h new file mode 100644 index 0000000000..1337822ecc --- /dev/null +++ b/esphome/components/http_request/update/http_request_update.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/http_request/ota/ota_http_request.h" +#include "esphome/components/update/update_entity.h" + +namespace esphome { +namespace http_request { + +class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { + public: + void setup() override; + void update() override; + + void perform() override; + + void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } + + void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; } + void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; } + + void set_current_version(const std::string ¤t_version) { this->current_version_ = current_version; } + + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + HttpRequestComponent *request_parent_; + OtaHttpRequestComponent *ota_parent_; + std::string source_url_; + std::string current_version_{""}; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 31cbb2cf97..96a02cb60e 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -126,6 +126,7 @@ MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent) MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent) +MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent) MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent) MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 66872680bb..0e063c66d2 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -137,6 +137,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls"; constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm"; constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd"; constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home"; +constexpr const char *const MQTT_PAYLOAD_INSTALL = "pl_inst"; constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc"; constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock"; constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd"; @@ -396,6 +397,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close"; constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm"; constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed"; constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home"; +constexpr const char *const MQTT_PAYLOAD_INSTALL = "payload_install"; constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate"; constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock"; constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed"; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp new file mode 100644 index 0000000000..2ed8faf074 --- /dev/null +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -0,0 +1,62 @@ +#include "mqtt_update.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_UPDATE + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.update"; + +using namespace esphome::update; + +MQTTUpdateComponent::MQTTUpdateComponent(UpdateEntity *update) : update_(update) {} + +void MQTTUpdateComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + if (payload == "INSTALL") { + this->update_->perform(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + this->status_momentary_warning("state", 5000); + } + }); + + this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); +} + +bool MQTTUpdateComponent::publish_state() { + return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { + root["installed_version"] = this->update_->update_info.current_version; + root["latest_version"] = this->update_->update_info.latest_version; + root["title"] = this->update_->update_info.title; + if (!this->update_->update_info.summary.empty()) + root["release_summary"] = this->update_->update_info.summary; + if (!this->update_->update_info.release_url.empty()) + root["release_url"] = this->update_->update_info.release_url; + }); +} + +void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + root["schema"] = "json"; + root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; +} + +bool MQTTUpdateComponent::send_initial_state() { return this->publish_state(); } + +void MQTTUpdateComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Update '%s': ", this->update_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); +} + +std::string MQTTUpdateComponent::component_type() const { return "update"; } +const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; } + +} // namespace mqtt +} // namespace esphome + +#endif // USE_UPDATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h new file mode 100644 index 0000000000..6fe04c4ea7 --- /dev/null +++ b/esphome/components/mqtt/mqtt_update.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_UPDATE + +#include "esphome/components/update/update_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTUpdateComponent : public mqtt::MQTTComponent { + public: + explicit MQTTUpdateComponent(update::UpdateEntity *update); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + protected: + /// "update" component type. + std::string component_type() const override; + const EntityBase *get_entity() const override; + + update::UpdateEntity *update_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_UPDATE +#endif // USE_MQTT diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py new file mode 100644 index 0000000000..ae3d5062ab --- /dev/null +++ b/esphome/components/update/__init__.py @@ -0,0 +1,108 @@ +from esphome import automation +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ID, + CONF_MQTT_ID, + CONF_WEB_SERVER_ID, + DEVICE_CLASS_FIRMWARE, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@jesserockz"] +IS_PLATFORM_COMPONENT = True + +update_ns = cg.esphome_ns.namespace("update") +UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase) + +UpdateInfo = update_ns.struct("UpdateInfo") + +PerformAction = update_ns.class_("PerformAction", automation.Action) +IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition) + +DEVICE_CLASSES = [ + DEVICE_CLASS_FIRMWARE, +] + +CONF_ON_UPDATE_AVAILABLE = "on_update_available" + +UPDATE_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTUpdateComponent), + cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional(CONF_ON_UPDATE_AVAILABLE): automation.validate_automation( + single=True + ), + } + ) +) + + +async def setup_update_core_(var, config): + await setup_entity(var, config) + + if device_class_config := config.get(CONF_DEVICE_CLASS): + cg.add(var.set_device_class(device_class_config)) + + if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): + await automation.build_automation( + var.get_update_available_trigger(), + [(UpdateInfo.operator("ref").operator("const"), "x")], + on_update_available, + ) + + if mqtt_id_config := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id_config, var) + await mqtt.register_mqtt_component(mqtt_, config) + + if web_server_id_config := config.get(CONF_WEB_SERVER_ID): + web_server_ = cg.get_variable(web_server_id_config) + web_server.add_entity_to_sorting_list(web_server_, var, config) + + +async def register_update(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_update(var)) + await setup_update_core_(var, config) + + +async def new_update(config): + var = cg.new_Pvariable(config[CONF_ID]) + await register_update(var, config) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_UPDATE") + cg.add_global(update_ns.using) + + +UPDATE_AUTOMATION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } +) + + +@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA) +async def update_perform_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, paren, paren) + + +@automation.register_condition( + "update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA +) +async def update_is_available_condition_to_code( + config, condition_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, paren, paren) diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp new file mode 100644 index 0000000000..501cb6635f --- /dev/null +++ b/esphome/components/update/update_entity.cpp @@ -0,0 +1,12 @@ +#include "update_entity.h" + +namespace esphome { +namespace update { + +void UpdateEntity::publish_state() { + this->has_state_ = true; + this->state_callback_.call(); +} + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h new file mode 100644 index 0000000000..5984c8e35b --- /dev/null +++ b/esphome/components/update/update_entity.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome { +namespace update { + +struct UpdateInfo { + std::string latest_version; + std::string current_version; + std::string title; + std::string summary; + std::string release_url; + std::string firmware_url; + std::string md5; + bool has_progress{false}; + float progress; +}; + +enum UpdateState : uint8_t { + UPDATE_STATE_UNKNOWN, + UPDATE_STATE_NO_UPDATE, + UPDATE_STATE_AVAILABLE, + UPDATE_STATE_INSTALLING, +}; + +class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { + public: + bool has_state() const { return this->has_state_; } + + void publish_state(); + + virtual void perform() = 0; + + const UpdateInfo &update_info = update_info_; + const UpdateState &state = state_; + + void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + + protected: + UpdateState state_{UPDATE_STATE_UNKNOWN}; + UpdateInfo update_info_; + bool has_state_{false}; + + CallbackManager state_callback_{}; +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 42af72e872..332f358352 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -177,5 +177,14 @@ bool ListEntitiesIterator::on_event(event::Event *event) { } #endif +#ifdef USE_UPDATE +bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->update_json(update, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 47d427d9b5..5ff6ec0412 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -68,6 +68,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_EVENT bool on_event(event::Event *event) override; #endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; +#endif protected: WebServer *web_server_; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5bb36f9600..9a1641e86f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1501,6 +1501,65 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty } #endif +#ifdef USE_UPDATE +void WebServer::on_update(update::UpdateEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->update_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (update::UpdateEntity *obj : App.get_updates()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + std::string data = this->update_json(obj, DETAIL_STATE); + request->send(200, "application/json", data.c_str()); + return; + } + + if (match.method != "install") { + request->send(404); + return; + } + + this->schedule_([obj]() mutable { obj->perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); + root["value"] = obj->update_info.latest_version; + switch (obj->state) { + case update::UPDATE_STATE_NO_UPDATE: + root["state"] = "NO UPDATE"; + break; + case update::UPDATE_STATE_AVAILABLE: + root["state"] = "UPDATE AVAILABLE"; + break; + case update::UPDATE_STATE_INSTALLING: + root["state"] = "INSTALLING"; + break; + default: + root["state"] = "UNKNOWN"; + break; + } + if (start_config == DETAIL_ALL) { + root["current_version"] = obj->update_info.current_version; + root["title"] = obj->update_info.title; + root["summary"] = obj->update_info.summary; + root["release_url"] = obj->update_info.release_url; + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + } + } + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -1620,6 +1679,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_UPDATE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "update") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -1777,6 +1841,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_UPDATE + if (match.domain == "update") { + this->handle_update_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 0fb40e2c33..5b98806af1 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -7,8 +7,8 @@ #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" -#include #include +#include #ifdef USE_ESP32 #include #include @@ -319,6 +319,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config); #endif +#ifdef USE_UPDATE + void on_update(update::UpdateEntity *obj) override; + + /// Handle a update request under '/update/'. + void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the update state with its value as a JSON string. + std::string update_json(update::UpdateEntity *obj, JsonDetail start_config); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/const.py b/esphome/const.py index dcb353ab1c..cbd932e3cc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1083,6 +1083,7 @@ DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_ENERGY_STORAGE = "energy_storage" +DEVICE_CLASS_FIRMWARE = "firmware" DEVICE_CLASS_FREQUENCY = "frequency" DEVICE_CLASS_GARAGE = "garage" DEVICE_CLASS_GARAGE_DOOR = "garage_door" diff --git a/esphome/core/application.h b/esphome/core/application.h index c4c745b687..2697357456 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -69,6 +69,9 @@ #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif namespace esphome { @@ -178,6 +181,10 @@ class Application { void register_event(event::Event *event) { this->events_.push_back(event); } #endif +#ifdef USE_UPDATE + void register_update(update::UpdateEntity *update) { this->updates_.push_back(update); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -421,6 +428,16 @@ class Application { } #endif +#ifdef USE_UPDATE + const std::vector &get_updates() { return this->updates_; } + update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->updates_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + Scheduler scheduler; protected: @@ -495,6 +512,9 @@ class Application { #ifdef USE_ALARM_CONTROL_PANEL std::vector alarm_control_panels_{}; #endif +#ifdef USE_UPDATE + std::vector updates_{}; +#endif std::string name_; std::string friendly_name_; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 9b02bf527b..da593340c1 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -351,6 +351,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_UPDATE + case IteratorState::UPDATE: + if (this->at_ >= App.get_updates().size()) { + advance_platform = true; + } else { + auto *update = App.get_updates()[this->at_]; + if (update->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_update(update); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 2b847bc088..9e187f6c57 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -86,6 +86,9 @@ class ComponentIterator { #endif #ifdef USE_EVENT virtual bool on_event(event::Event *event) = 0; +#endif +#ifdef USE_UPDATE + virtual bool on_update(update::UpdateEntity *update) = 0; #endif virtual bool on_end(); @@ -158,6 +161,9 @@ class ComponentIterator { #endif #ifdef USE_EVENT EVENT, +#endif +#ifdef USE_UPDATE + UPDATE, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 0957329500..d6d98a4316 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -121,6 +121,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); } #endif +#ifdef USE_UPDATE + for (auto *obj : App.get_updates()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index e1bf93193a..39e0b2ba26 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -61,6 +61,9 @@ #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif namespace esphome { @@ -124,6 +127,9 @@ class Controller { #ifdef USE_EVENT virtual void on_event(event::Event *obj, const std::string &event_type){}; #endif +#ifdef USE_UPDATE + virtual void on_update(update::UpdateEntity *obj){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index affcd78089..1e6f3517db 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -59,6 +59,7 @@ #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER +#define USE_UPDATE #define USE_VALVE #define USE_WIFI #define USE_WIFI_AP diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml index 2b6996c0b9..83b334ca2d 100644 --- a/tests/components/http_request/common.yaml +++ b/tests/components/http_request/common.yaml @@ -73,3 +73,9 @@ button: url: http://my.ha.net:8123/local/esphome/firmware.bin - logger.log: "This message should be not displayed (reboot)" + +update: + - platform: http_request + name: OTA Update + id: ota_update + source: http://my.ha.net:8123/local/esphome/manifest.json diff --git a/tests/components/mqtt/common-update.yaml b/tests/components/mqtt/common-update.yaml new file mode 100644 index 0000000000..25f57cfef2 --- /dev/null +++ b/tests/components/mqtt/common-update.yaml @@ -0,0 +1,13 @@ +substitutions: + verify_ssl: "true" + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + +update: + - platform: http_request + name: "OTA Update" + source: https://example.com/ota.json diff --git a/tests/components/mqtt/test.esp32-c3-idf.yaml b/tests/components/mqtt/test.esp32-c3-idf.yaml index 25cb37a0b4..d19609b55e 100644 --- a/tests/components/mqtt/test.esp32-c3-idf.yaml +++ b/tests/components/mqtt/test.esp32-c3-idf.yaml @@ -1,2 +1,3 @@ packages: common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-c3.yaml b/tests/components/mqtt/test.esp32-c3.yaml index 25cb37a0b4..4c70fb37d9 100644 --- a/tests/components/mqtt/test.esp32-c3.yaml +++ b/tests/components/mqtt/test.esp32-c3.yaml @@ -1,2 +1,6 @@ +substitutions: + verify_ssl: "false" + packages: common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-idf.yaml b/tests/components/mqtt/test.esp32-idf.yaml index 25cb37a0b4..d19609b55e 100644 --- a/tests/components/mqtt/test.esp32-idf.yaml +++ b/tests/components/mqtt/test.esp32-idf.yaml @@ -1,2 +1,3 @@ packages: common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32.yaml b/tests/components/mqtt/test.esp32.yaml index 25cb37a0b4..4c70fb37d9 100644 --- a/tests/components/mqtt/test.esp32.yaml +++ b/tests/components/mqtt/test.esp32.yaml @@ -1,2 +1,6 @@ +substitutions: + verify_ssl: "false" + packages: common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp8266.yaml b/tests/components/mqtt/test.esp8266.yaml index 25cb37a0b4..4c70fb37d9 100644 --- a/tests/components/mqtt/test.esp8266.yaml +++ b/tests/components/mqtt/test.esp8266.yaml @@ -1,2 +1,6 @@ +substitutions: + verify_ssl: "false" + packages: common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml new file mode 100644 index 0000000000..91b8669505 --- /dev/null +++ b/tests/components/update/common.yaml @@ -0,0 +1 @@ +update: diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/update/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/test.esp32-idf.yaml b/tests/components/update/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/update/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/test.esp8266.yaml b/tests/components/update/test.esp8266.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/update/test.esp8266.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/test.rp2040.yaml b/tests/components/update/test.rp2040.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/update/test.rp2040.yaml @@ -0,0 +1 @@ +<<: !include common.yaml