[core] Update Entities (#6885)

This commit is contained in:
Jesse Hills 2024-06-12 09:57:36 +12:00 committed by GitHub
parent 7dc07c5632
commit 3cd2fb0843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1191 additions and 2 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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)

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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_;

View file

@ -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);

View file

@ -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 {

View 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)

View 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

View 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 &current_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

View file

@ -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")

View file

@ -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";

View 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

View 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

View 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)

View 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

View 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

View file

@ -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

View file

@ -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_;

View file

@ -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; }

View file

@ -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.

View file

@ -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"

View file

@ -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_;

View file

@ -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()) {

View file

@ -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};

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

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

View file

@ -1,2 +1,6 @@
substitutions:
verify_ssl: "false"
packages:
common: !include common.yaml
update: !include common-update.yaml

View file

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

View file

@ -1,2 +1,6 @@
substitutions:
verify_ssl: "false"
packages:
common: !include common.yaml
update: !include common-update.yaml

View file

@ -1,2 +1,6 @@
substitutions:
verify_ssl: "false"
packages:
common: !include common.yaml
update: !include common-update.yaml

View file

@ -0,0 +1 @@
update:

View file

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

View file

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

View file

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

View file

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