mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[core] Update Entities (#6885)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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<enums::EntityCategory>(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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<enums::EntityCategory>(); | ||||
|       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<enums::EntityCategory>(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<enums::EntityCategory>(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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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_<ListEntitiesUpdateResponse>(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_<UpdateStateResponse>(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) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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_; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										44
									
								
								esphome/components/http_request/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/http_request/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										157
									
								
								esphome/components/http_request/update/http_request_update.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/http_request/update/http_request_update.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::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<std::string>(); | ||||
|     this->update_info_.latest_version = root["version"].as<std::string>(); | ||||
|  | ||||
|     for (auto build : root["builds"].as<JsonArray>()) { | ||||
|       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<std::string>(); | ||||
|         this->update_info_.md5 = ota["md5"].as<std::string>(); | ||||
|  | ||||
|         if (ota.containsKey("summary")) | ||||
|           this->update_info_.summary = ota["summary"].as<std::string>(); | ||||
|         if (ota.containsKey("release_url")) | ||||
|           this->update_info_.release_url = ota["release_url"].as<std::string>(); | ||||
|  | ||||
|         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 | ||||
							
								
								
									
										37
									
								
								esphome/components/http_request/update/http_request_update.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/http_request/update/http_request_update.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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"; | ||||
|   | ||||
							
								
								
									
										62
									
								
								esphome/components/mqtt/mqtt_update.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/mqtt/mqtt_update.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										41
									
								
								esphome/components/mqtt/mqtt_update.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/mqtt/mqtt_update.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										108
									
								
								esphome/components/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								esphome/components/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										12
									
								
								esphome/components/update/update_entity.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/update/update_entity.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										51
									
								
								esphome/components/update/update_entity.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/update/update_entity.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<void()> &&callback) { this->state_callback_.add(std::move(callback)); } | ||||
|  | ||||
|  protected: | ||||
|   UpdateState state_{UPDATE_STATE_UNKNOWN}; | ||||
|   UpdateInfo update_info_; | ||||
|   bool has_state_{false}; | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace update | ||||
| }  // namespace esphome | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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_; | ||||
|   | ||||
| @@ -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; } | ||||
|   | ||||
| @@ -7,8 +7,8 @@ | ||||
| #include "esphome/core/controller.h" | ||||
| #include "esphome/core/entity_base.h" | ||||
|  | ||||
| #include <vector> | ||||
| #include <map> | ||||
| #include <vector> | ||||
| #ifdef USE_ESP32 | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/semphr.h> | ||||
| @@ -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/<id>'. | ||||
|   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. | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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<class C> C *register_component(C *c) { | ||||
|     static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered"); | ||||
| @@ -421,6 +428,16 @@ class Application { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|   const std::vector<update::UpdateEntity *> &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_panel::AlarmControlPanel *> alarm_control_panels_{}; | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
|   std::vector<update::UpdateEntity *> updates_{}; | ||||
| #endif | ||||
|  | ||||
|   std::string name_; | ||||
|   std::string friendly_name_; | ||||
|   | ||||
| @@ -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()) { | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										13
									
								
								tests/components/mqtt/common-update.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/components/mqtt/common-update.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -1,2 +1,3 @@ | ||||
| packages: | ||||
|   common: !include common.yaml | ||||
|   update: !include common-update.yaml | ||||
|   | ||||
| @@ -1,2 +1,6 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| packages: | ||||
|   common: !include common.yaml | ||||
|   update: !include common-update.yaml | ||||
|   | ||||
| @@ -1,2 +1,3 @@ | ||||
| packages: | ||||
|   common: !include common.yaml | ||||
|   update: !include common-update.yaml | ||||
|   | ||||
| @@ -1,2 +1,6 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| packages: | ||||
|   common: !include common.yaml | ||||
|   update: !include common-update.yaml | ||||
|   | ||||
| @@ -1,2 +1,6 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| packages: | ||||
|   common: !include common.yaml | ||||
|   update: !include common-update.yaml | ||||
|   | ||||
							
								
								
									
										1
									
								
								tests/components/update/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/update/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| update: | ||||
							
								
								
									
										1
									
								
								tests/components/update/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/update/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										1
									
								
								tests/components/update/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/update/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										1
									
								
								tests/components/update/test.esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/update/test.esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										1
									
								
								tests/components/update/test.rp2040.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/update/test.rp2040.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
		Reference in New Issue
	
	Block a user