From 8a055675af8e428bf7b8338cd0b22d01c252c6dc Mon Sep 17 00:00:00 2001 From: lkomurcu <44089359+lkomurcu@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:39:40 +0200 Subject: [PATCH 001/106] Move gas mbus config option being a define to being a build flag since its used in external libraries. (#3575) --- esphome/components/dsmr/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 7a7681082e..624a0f35a1 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -79,7 +79,7 @@ async def to_code(config): cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) - cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) + cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) # DSMR Parser cg.add_library("glmnet/Dsmr", "0.5") From d0c646c72162ca0421df84bd941d54735282db52 Mon Sep 17 00:00:00 2001 From: ShellAddicted Date: Mon, 20 Jun 2022 01:17:58 +0200 Subject: [PATCH 002/106] Fix: Make MQTT over TLS actually work (#3580) --- esphome/components/mqtt/mqtt_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 157504fb41..aa0cf56c51 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -187,7 +187,7 @@ void MQTTClientComponent::start_connect_() { this->mqtt_backend_.set_credentials(username, password); - this->mqtt_backend_.set_server((uint32_t) this->ip_, this->credentials_.port); + this->mqtt_backend_.set_server(this->credentials_.address.c_str(), this->credentials_.port); if (!this->last_will_.topic.empty()) { this->mqtt_backend_.set_will(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain, this->last_will_.payload.c_str()); From ca13c4c1a657cf614379125e15a679213923abc7 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Sun, 19 Jun 2022 19:27:00 -0400 Subject: [PATCH 003/106] Fix wrong type for voc_state*_ in sgp4x component (#3581) Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sgp4x/sensor.py | 4 +++- esphome/components/sgp4x/sgp4x.cpp | 4 ++-- esphome/components/sgp4x/sgp4x.h | 6 +++--- platformio.ini | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 4855d7f066..1f6c5006a5 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -140,5 +140,7 @@ async def to_code(config): ) ) cg.add_library( - None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git" + None, + None, + "https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1", ) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index a6f57e0342..257c30075f 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -170,8 +170,8 @@ bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) { // much if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); - if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || - (uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { + if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || + std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { this->seconds_since_last_store_ = 0; this->voc_baselines_storage_.state0 = this->voc_state0_; this->voc_baselines_storage_.state1 = this->voc_state1_; diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 3060972fc3..3a8d8200a7 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -49,7 +49,7 @@ static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test static const uint16_t SGP40_MEASURE_TIME = 30; static const uint16_t SGP41_MEASURE_TIME = 55; // Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; +const float MAXIMUM_STORAGE_DIFF = 50.0f; class SGP4xComponent; @@ -120,8 +120,8 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se sensor::Sensor *voc_sensor_{nullptr}; VOCGasIndexAlgorithm voc_algorithm_; optional voc_tuning_params_; - int32_t voc_state0_; - int32_t voc_state1_; + float voc_state0_; + float voc_state1_; int32_t voc_index_ = 0; sensor::Sensor *nox_sensor_{nullptr}; diff --git a/platformio.ini b/platformio.ini index fb33087a8a..733e942a84 100644 --- a/platformio.ini +++ b/platformio.ini @@ -40,7 +40,7 @@ lib_deps = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 ; This is using the repository until a new release is published to PlatformIO - https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library + https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = From 8f67acadd817807f07a47fd883d307ffa7c15002 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 20 Jun 2022 04:17:20 +0400 Subject: [PATCH 004/106] Media Player: added play_media action (#3579) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/media_player/__init__.py | 31 ++++++++++++++++++++ esphome/components/media_player/automation.h | 19 ++++++++++-- tests/test4.yaml | 6 ++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 3456417370..80f5fc558a 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -20,6 +20,9 @@ MediaPlayer = media_player_ns.class_("MediaPlayer") PlayAction = media_player_ns.class_( "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) ) +PlayMediaAction = media_player_ns.class_( + "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) +) ToggleAction = media_player_ns.class_( "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) ) @@ -44,11 +47,14 @@ CONF_VOLUME = "volume" CONF_ON_IDLE = "on_idle" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" +CONF_MEDIA_URL = "media_url" StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) +IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) +IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) async def setup_media_player_core_(var, config): @@ -103,6 +109,25 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)}) +@automation.register_action( + "media_player.play_media", + PlayMediaAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MediaPlayer), + cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), + }, + key=CONF_MEDIA_URL, + ), +) +async def media_player_play_media_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) + cg.add(var.set_media_url(media_url)) + return var + + @automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) @automation.register_action( "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA @@ -117,6 +142,12 @@ MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPl @automation.register_action( "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA ) +@automation.register_condition( + "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA +) +@automation.register_condition( + "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA +) async def media_player_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index c6deba7dc6..261e93775c 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -17,7 +17,7 @@ namespace media_player { #define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \ class TRIGGER_CLASS : public Trigger<> { \ public: \ - TRIGGER_CLASS(MediaPlayer *player) { \ + explicit TRIGGER_CLASS(MediaPlayer *player) { \ player->add_on_state_callback([this, player]() { \ if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \ this->trigger(); \ @@ -32,6 +32,11 @@ MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE) MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP) MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN) +template class PlayMediaAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, media_url) + void play(Ts... x) override { this->parent_->make_call().set_media_url(this->media_url_.value(x...)).perform(); } +}; + template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } @@ -39,7 +44,7 @@ template class VolumeSetAction : public Action, public Pa class StateTrigger : public Trigger<> { public: - StateTrigger(MediaPlayer *player) { + explicit StateTrigger(MediaPlayer *player) { player->add_on_state_callback([this]() { this->trigger(); }); } }; @@ -48,5 +53,15 @@ MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE) MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING) MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED) +template class IsIdleCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } +}; + } // namespace media_player } // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index c82673b196..0dfbeed550 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -615,12 +615,18 @@ media_player: mute_pin: GPIO14 on_state: - media_player.play: + - media_player.play_media: http://localhost/media.mp3 + - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' on_idle: - media_player.pause: on_play: - media_player.stop: on_pause: - media_player.toggle: + - wait_until: + media_player.is_idle: + - wait_until: + media_player.is_playing: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% From 4c37c17df129698e6962bfab52358cc0a16f8f41 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 20 Jun 2022 19:31:17 -0400 Subject: [PATCH 005/106] Fix 2 small issues in BLEClient (#3544) --- esphome/components/ble_client/ble_client.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 7bef0d652c..d06c3c4cad 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -113,6 +113,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } case ESP_GATTC_OPEN_EVT: { ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + this->conn_id = param->open.conn_id; if (param->open.status != ESP_GATT_OK) { ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); this->set_states_(espbt::ClientState::IDLE); @@ -122,7 +123,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } case ESP_GATTC_CONNECT_EVT: { ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str()); - this->conn_id = param->connect.conn_id; + if (this->conn_id != param->connect.conn_id) { + ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d", + this->address_str().c_str(), param->connect.conn_id, this->conn_id); + } auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id); if (ret) { ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret); @@ -183,9 +187,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es descr->uuid.to_string().c_str()); break; } - uint8_t notify_en = 1; - auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), - ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + uint16_t notify_en = 1; + auto status = + esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), + (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); } From 020b2c05c828ca0faea7676ff73e8df37674cbb4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Jun 2022 10:34:10 +1200 Subject: [PATCH 006/106] Bump version to 2022.6.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index cdb923a107..f7b593b541 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.6.1" +__version__ = "2022.6.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From df73170e5ae387960baeb8a27c14d962bad00119 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 24 Jul 2022 22:06:18 +0200 Subject: [PATCH 007/106] modbus: fix queue deduplicator erasing custom commands (#3650) --- .../modbus_controller/modbus_controller.cpp | 12 ++++++++++-- .../components/modbus_controller/modbus_controller.h | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 91e0dcc45f..b7fde157d8 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -108,8 +108,7 @@ void ModbusController::queue_command(const ModbusCommandItem &command) { // check if this commmand is already qeued. // not very effective but the queue is never really large for (auto &item : command_queue_) { - if (item->register_address == command.register_address && item->register_count == command.register_count && - item->register_type == command.register_type && item->function_code == command.function_code) { + if (item->is_equal(command)) { ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", static_cast(command.register_type), command.register_address, command.register_count); // update the payload of the queued command @@ -489,6 +488,15 @@ bool ModbusCommandItem::send() { return true; } +bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { + // for custom commands we have to check for identical payloads, since + // address/count/type fields will be set to zero + return this->function_code == ModbusFunctionCode::CUSTOM + ? this->payload == other.payload + : other.register_address == this->register_address && other.register_count == this->register_count && + other.register_type == this->register_type && other.function_code == this->function_code; +} + void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { switch (value_type) { case SensorValueType::U_WORD: diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 6aecf7f8a4..f67242a68e 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -395,6 +395,8 @@ class ModbusCommandItem { ModbusController *modbusdevice, const std::vector &values, std::function &data)> &&handler = nullptr); + + bool is_equal(const ModbusCommandItem &other); }; /** Modbus controller class. From 311980e0e478612a01020758d2b71bec2cd02fdc Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 31 Jul 2022 18:08:19 -0700 Subject: [PATCH 008/106] Update inkbird_ibsth1_mini.cpp (#3664) --- esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 76013e28ff..94c22ae84d 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -55,7 +55,7 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic ESP_LOGVV(TAG, "parse_device(): manufacturer data element length is expected to be of length 7"); return false; } - if (mnf_data.data[6] != 8) { + if ((mnf_data.data[6] != 8) && (mnf_data.data[6] != 6)) { ESP_LOGVV(TAG, "parse_device(): unexpected data"); return false; } From eb878710c1e6f750c0ca532ee4d258447bac30d6 Mon Sep 17 00:00:00 2001 From: Bryan Berg Date: Mon, 1 Aug 2022 16:32:02 -0700 Subject: [PATCH 009/106] Add CO device class to binary_sensor (#3656) --- esphome/components/binary_sensor/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 40f95d72f9..63fd34c7d5 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, @@ -63,6 +64,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, From 50f32a3aa59cf31cf55abc73543ee7f09fa7d96e Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 1 Aug 2022 22:07:32 -0700 Subject: [PATCH 010/106] Use application/json instead of text/json (#3671) --- esphome/components/web_server/web_server.cpp | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 18374d606b..f0a7efd12f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -353,7 +353,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; std::string data = this->sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } request->send(404); @@ -377,7 +377,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const if (obj->get_object_id() != match.id) continue; std::string data = this->text_sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } request->send(404); @@ -406,7 +406,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET) { std::string data = this->switch_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle(); }); request->send(200); @@ -462,7 +462,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con if (obj->get_object_id() != match.id) continue; std::string data = this->binary_sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } request->send(404); @@ -490,7 +490,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET) { std::string data = this->fan_json(obj, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); @@ -551,7 +551,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET) { std::string data = this->light_json(obj, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); @@ -630,7 +630,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET) { std::string data = this->cover_json(obj, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); continue; } @@ -687,7 +687,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET) { std::string data = this->number_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } if (match.method != "set") { @@ -741,7 +741,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET) { std::string data = this->select_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } @@ -788,7 +788,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET) { std::string data = this->climate_json(obj, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); return; } @@ -926,7 +926,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET) { std::string data = this->lock_json(obj, obj->state, DETAIL_STATE); - request->send(200, "text/json", data.c_str()); + request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { this->defer([obj]() { obj->lock(); }); request->send(200); From e5eaf7a3fecfbb9818890aed7ef7a56cce41ae0e Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 1 Aug 2022 22:09:17 -0700 Subject: [PATCH 011/106] Use correct struct members. (#3672) --- esphome/components/remote_base/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 165da95d67..5ccfc500cf 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -744,7 +744,8 @@ def rc6_binary_sensor(var, config): var.set_data( cg.StructInitializer( RC6Data, - ("device", config[CONF_DEVICE]), + ("mode", 0), + ("toggle", 0), ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), ) From bf8eddb13b4a5846c1e6bd21fb574e97e7bce684 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 8 Aug 2022 08:24:12 +1200 Subject: [PATCH 012/106] Bump version to 2022.6.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f7b593b541..f95628735d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.6.2" +__version__ = "2022.6.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3a82f500d4d1a8122aeb8ba4dd1a228a36ed2101 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Aug 2022 12:58:30 +1200 Subject: [PATCH 013/106] Bump version to 2022.8.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 00d155e66f..981ee773d4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.8.0b3" +__version__ = "2022.8.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3acc8e7479843998bdc9def5ea0a0c7397906b57 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 16 Aug 2022 20:41:46 -0700 Subject: [PATCH 014/106] fix grow password setting (#3722) Co-authored-by: Samuel Sieb --- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 8 ++++---- esphome/components/fingerprint_grow/fingerprint_grow.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 3b8c52fea2..1d6cb776b7 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -51,7 +51,7 @@ void FingerprintGrowComponent::update() { void FingerprintGrowComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); if (this->check_password_()) { - if (this->new_password_ != nullptr) { + if (this->new_password_ != -1) { if (this->set_password_()) return; } else { @@ -202,9 +202,9 @@ bool FingerprintGrowComponent::check_password_() { } bool FingerprintGrowComponent::set_password_() { - ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); - this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), - (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; + ESP_LOGI(TAG, "Setting new password: %d", this->new_password_); + this->data_ = {SET_PASSWORD, (uint8_t)(this->new_password_ >> 24), (uint8_t)(this->new_password_ >> 16), + (uint8_t)(this->new_password_ >> 8), (uint8_t)(this->new_password_ & 0xFF)}; if (this->send_command_() == OK) { ESP_LOGI(TAG, "New password successfully set"); ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 7ec253ff3a..e50418a768 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -96,7 +96,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic } void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } void set_password(uint32_t password) { this->password_ = password; } - void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } + void set_new_password(uint32_t new_password) { this->new_password_ = new_password; } void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { this->fingerprint_count_sensor_ = fingerprint_count_sensor; } @@ -153,7 +153,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint16_t capacity_ = 64; uint32_t password_ = 0x0; - uint32_t *new_password_{nullptr}; + uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; uint8_t enrollment_image_ = 0; uint16_t enrollment_slot_ = 0; From 22eb4f9cb92ee2a22397f187c15d279c46e2552a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 18 Aug 2022 14:55:10 -0500 Subject: [PATCH 015/106] Fix SPI HW selection for ESP32 variants (#3728) --- esphome/components/spi/spi.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 95280f23b9..864f6ae39d 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -76,7 +76,11 @@ void SPIComponent::setup() { if (spi_bus_num == 0) { this->hw_spi_ = &SPI; } else { +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + this->hw_spi_ = new SPIClass(FSPI); // NOLINT(cppcoreguidelines-owning-memory) +#else this->hw_spi_ = new SPIClass(VSPI); // NOLINT(cppcoreguidelines-owning-memory) +#endif // USE_ESP32_VARIANT } spi_bus_num++; this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); From aa2eb29274a7c3c7be4bf97dfe13e4a070cb81a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 1 Sep 2022 11:26:52 +1200 Subject: [PATCH 016/106] Bump version to 2022.8.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 981ee773d4..3091f55f12 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.8.0" +__version__ = "2022.8.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From c8eb30ef27934329f54a1fce50444400239acda8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 25 Aug 2022 07:13:44 +1200 Subject: [PATCH 017/106] Initial bluetooth_proxy support (#3736) --- CODEOWNERS | 1 + esphome/components/api/api.proto | 28 ++++ esphome/components/api/api_connection.cpp | 3 + esphome/components/api/api_connection.h | 15 ++ esphome/components/api/api_pb2.cpp | 146 ++++++++++++++++++ esphome/components/api/api_pb2.h | 40 +++++ esphome/components/api/api_pb2_service.cpp | 29 ++++ esphome/components/api/api_pb2_service.h | 7 + esphome/components/api/api_server.cpp | 7 + esphome/components/api/api_server.h | 3 + esphome/components/api/proto.h | 2 +- .../components/bluetooth_proxy/__init__.py | 27 ++++ .../bluetooth_proxy/bluetooth_proxy.cpp | 58 +++++++ .../bluetooth_proxy/bluetooth_proxy.h | 26 ++++ esphome/core/defines.h | 1 + tests/test1.yaml | 2 + 16 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 esphome/components/bluetooth_proxy/__init__.py create mode 100644 esphome/components/bluetooth_proxy/bluetooth_proxy.cpp create mode 100644 esphome/components/bluetooth_proxy/bluetooth_proxy.h diff --git a/CODEOWNERS b/CODEOWNERS index 62aae6d3cb..7f7562dec4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -34,6 +34,7 @@ esphome/components/binary_sensor/* @esphome/core esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix +esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmp3xx/* @martgras esphome/components/button/* @esphome/core diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 88a74540d0..c6cde8b038 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -27,6 +27,7 @@ service APIConnection { rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} + rpc subscribe_bluetooth_le_advertisements (SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc get_time (GetTimeRequest) returns (GetTimeResponse) { option (needs_authentication) = false; } @@ -190,6 +191,8 @@ message DeviceInfoResponse { string project_version = 9; uint32 webserver_port = 10; + + bool has_bluetooth_proxy = 11; } message ListEntitiesRequest { @@ -1099,3 +1102,28 @@ message MediaPlayerCommandRequest { bool has_media_url = 6; string media_url = 7; } + +// ==================== BLUETOOTH ==================== +message SubscribeBluetoothLEAdvertisementsRequest { + option (id) = 66; + option (source) = SOURCE_CLIENT; +} + +message BluetoothServiceData { + string uuid = 1; + repeated uint32 data = 2 [packed=false]; +} +message BluetoothLEAdvertisementResponse { + option (id) = 67; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + uint64 address = 1; + string name = 2; + sint32 rssi = 3; + + repeated string service_uuids = 4; + repeated BluetoothServiceData service_data = 5; + repeated BluetoothServiceData manufacturer_data = 6; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9028034c90..a3b000c778 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -886,6 +886,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #endif #ifdef USE_WEBSERVER resp.webserver_port = USE_WEBSERVER_PORT; +#endif +#ifdef USE_BLUETOOTH_PROXY + resp.has_bluetooth_proxy = true; #endif return resp; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0787d2f7eb..dcf8bacad2 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -7,6 +7,10 @@ #include "api_server.h" #include "api_frame_helper.h" +#ifdef USE_BLUETOOTH_PROXY +#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" +#endif + namespace esphome { namespace api { @@ -94,6 +98,13 @@ class APIConnection : public APIServerConnection { return; this->send_homeassistant_service_response(call); } +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { + if (!this->bluetooth_le_advertisement_subscription_) + return false; + return this->send_bluetooth_le_advertisement_response(call); + } +#endif #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; @@ -134,6 +145,9 @@ class APIConnection : public APIServerConnection { return {}; } void execute_service(const ExecuteServiceRequest &msg) override; + void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { + this->bluetooth_le_advertisement_subscription_ = true; + } bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } bool is_connection_setup() override { return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); @@ -176,6 +190,7 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; + bool bluetooth_le_advertisement_subscription_{true}; bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b91c9bd600..13969fce76 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -495,6 +495,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->webserver_port = value.as_uint32(); return true; } + case 11: { + this->has_bluetooth_proxy = value.as_bool(); + return true; + } default: return false; } @@ -544,6 +548,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); buffer.encode_uint32(10, this->webserver_port); + buffer.encode_bool(11, this->has_bluetooth_proxy); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -589,6 +594,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { sprintf(buffer, "%u", this->webserver_port); out.append(buffer); out.append("\n"); + + out.append(" has_bluetooth_proxy: "); + out.append(YESNO(this->has_bluetooth_proxy)); + out.append("\n"); out.append("}"); } #endif @@ -4854,6 +4863,143 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { + out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); +} +#endif +bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->data.push_back(value.as_uint32()); + return true; + } + default: + return false; + } +} +bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->uuid = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->uuid); + for (auto &it : this->data) { + buffer.encode_uint32(2, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothServiceData::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothServiceData {\n"); + out.append(" uuid: "); + out.append("'").append(this->uuid).append("'"); + out.append("\n"); + + for (const auto &it : this->data) { + out.append(" data: "); + sprintf(buffer, "%u", it); + out.append(buffer); + out.append("\n"); + } + out.append("}"); +} +#endif +bool BluetoothLEAdvertisementResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 3: { + this->rssi = value.as_sint32(); + return true; + } + default: + return false; + } +} +bool BluetoothLEAdvertisementResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + case 4: { + this->service_uuids.push_back(value.as_string()); + return true; + } + case 5: { + this->service_data.push_back(value.as_message()); + return true; + } + case 6: { + this->manufacturer_data.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_string(2, this->name); + buffer.encode_sint32(3, this->rssi); + for (auto &it : this->service_uuids) { + buffer.encode_string(4, it, true); + } + for (auto &it : this->service_data) { + buffer.encode_message(5, it, true); + } + for (auto &it : this->manufacturer_data) { + buffer.encode_message(6, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLEAdvertisementResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" rssi: "); + sprintf(buffer, "%d", this->rssi); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->service_uuids) { + out.append(" service_uuids: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + for (const auto &it : this->service_data) { + out.append(" service_data: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->manufacturer_data) { + out.append(" manufacturer_data: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index f9981fdbb7..2093f93ee7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -263,6 +263,7 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; uint32_t webserver_port{0}; + bool has_bluetooth_proxy{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1214,6 +1215,45 @@ class MediaPlayerCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class BluetoothServiceData : public ProtoMessage { + public: + std::string uuid{}; + std::vector data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothLEAdvertisementResponse : public ProtoMessage { + public: + uint64_t address{0}; + std::string name{}; + int32_t rssi{0}; + std::vector service_uuids{}; + std::vector service_data{}; + std::vector manufacturer_data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index bd146cb54d..6932675c41 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -328,6 +328,14 @@ bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayer #endif #ifdef USE_MEDIA_PLAYER #endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_le_advertisement_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 67); +} +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -595,6 +603,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #endif break; } + case 66: { + SubscribeBluetoothLEAdvertisementsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); +#endif + this->on_subscribe_bluetooth_le_advertisements_request(msg); + break; + } default: return false; } @@ -691,6 +708,18 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc } this->subscribe_home_assistant_states(msg); } +void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( + const SubscribeBluetoothLEAdvertisementsRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_bluetooth_le_advertisements(msg); +} void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { if (!this->is_connection_setup()) { this->on_no_setup_connection(); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 28ad3fbd15..49426d1bbc 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -153,6 +153,11 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_MEDIA_PLAYER virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; +#endif + virtual void on_subscribe_bluetooth_le_advertisements_request( + const SubscribeBluetoothLEAdvertisementsRequest &value){}; +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -170,6 +175,7 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; + virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #ifdef USE_COVER @@ -216,6 +222,7 @@ class APIServerConnection : public APIServerConnectionBase { void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override; #ifdef USE_COVER diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 8375a82313..5851594955 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -291,6 +291,13 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon client->send_homeassistant_service_call(call); } } +#ifdef USE_BLUETOOTH_PROXY +void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_le_advertisement(call); + } +} +#endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6997e23cac..dc892b2088 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -73,6 +73,9 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); +#ifdef USE_BLUETOOTH_PROXY + void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); +#endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME void request_time(); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 32f525990d..11cd4330ce 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -70,7 +70,7 @@ class ProtoVarInt { } } void encode(std::vector &out) { - uint32_t val = this->value_; + uint64_t val = this->value_; if (val <= 0x7F) { out.push_back(val); return; diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py new file mode 100644 index 0000000000..a6a5a4391b --- /dev/null +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -0,0 +1,27 @@ +from esphome.components import esp32_ble_tracker +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32", "esp32_ble_tracker"] +CODEOWNERS = ["@jesserockz"] + + +bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") + +BluetoothProxy = bluetooth_proxy_ns.class_("BluetoothProxy", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BluetoothProxy), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add_define("USE_BLUETOOTH_PROXY") diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp new file mode 100644 index 0000000000..41871295ce --- /dev/null +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -0,0 +1,58 @@ +#include "bluetooth_proxy.h" + +#ifdef USE_API +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_server.h" +#endif // USE_API +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bluetooth_proxy { + +static const char *const TAG = "bluetooth_proxy"; + +bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), + device.get_rssi()); + this->send_api_packet_(device); + return true; +} + +void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { +#ifndef USE_API + return; +#else + api::BluetoothLEAdvertisementResponse resp; + resp.address = device.address_uint64(); + if (!device.get_name().empty()) + resp.name = device.get_name(); + resp.rssi = device.get_rssi(); + for (auto uuid : device.get_service_uuids()) { + resp.service_uuids.push_back(uuid.to_string()); + } + for (auto &data : device.get_service_datas()) { + api::BluetoothServiceData service_data; + service_data.uuid = data.uuid.to_string(); + for (auto d : data.data) + service_data.data.push_back(d); + resp.service_data.push_back(service_data); + } + for (auto &data : device.get_manufacturer_datas()) { + api::BluetoothServiceData manufacturer_data; + manufacturer_data.uuid = data.uuid.to_string(); + for (auto d : data.data) + manufacturer_data.data.push_back(d); + resp.manufacturer_data.push_back(manufacturer_data); + } + api::global_api_server->send_bluetooth_le_advertisement(resp); +#endif +} + +void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); } + +} // namespace bluetooth_proxy +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h new file mode 100644 index 0000000000..9a936747c0 --- /dev/null +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef USE_ESP32 + +#include + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace bluetooth_proxy { + +class BluetoothProxy : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + + protected: + void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); +}; + +} // namespace bluetooth_proxy +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5b1d567aa4..51da2ceb66 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -70,6 +70,7 @@ #define USE_ESP32_IGNORE_EFUSE_MAC_CRC #define USE_IMPROV #define USE_SOCKET_IMPL_BSD_SOCKETS +#define USE_BLUETOOTH_PROXY #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 6) diff --git a/tests/test1.yaml b/tests/test1.yaml index 5897639ead..15ede4d05e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -288,6 +288,8 @@ adalight: esp32_ble_tracker: +bluetooth_proxy: + ble_client: - mac_address: AA:BB:CC:DD:EE:FF id: ble_foo From d2ce62aa13714c8f76bd74d9b03c93985205a343 Mon Sep 17 00:00:00 2001 From: anatoly-savchenkov <48646998+anatoly-savchenkov@users.noreply.github.com> Date: Sun, 4 Sep 2022 10:21:17 +0300 Subject: [PATCH 018/106] Ignore NaN states in the integration component (#3767) --- esphome/components/integration/integration_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 65fa42dd0d..2ac7caca21 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -23,6 +23,8 @@ void IntegrationSensor::setup() { } void IntegrationSensor::dump_config() { LOG_SENSOR("", "Integration Sensor", this); } void IntegrationSensor::process_sensor_value_(float value) { + if (std::isnan(value)) + return; const uint32_t now = millis(); const double old_value = this->last_value_; const double new_value = value; From 39d493c2786d301797ec19a46fa45914710d226c Mon Sep 17 00:00:00 2001 From: Avirsaam <107193364+Avirsaam@users.noreply.github.com> Date: Mon, 5 Sep 2022 03:50:27 +0300 Subject: [PATCH 019/106] Update modbus_controller.cpp (#3768) --- esphome/components/modbus_controller/modbus_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index bd2fec3ece..e60b016a17 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -236,7 +236,7 @@ size_t ModbusController::create_register_ranges_() { } } - if (curr->start_address == r.start_address) { + if (curr->start_address == r.start_address && curr->register_type == r.register_type) { // use the lowest non zero value for the whole range // Because zero is the default value for skip_updates it is excluded from getting the min value. if (curr->skip_updates != 0) { From 5bf0c92318b463968bdf070835070fb2112e43a9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:12:00 +1200 Subject: [PATCH 020/106] Bump version to 2022.8.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3091f55f12..7d916e30af 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.8.1" +__version__ = "2022.8.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From acd0b50b40115d63cc345b3211ad551cee764307 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Sep 2022 16:41:23 +1200 Subject: [PATCH 021/106] Fix HA addon auth using HA credentials (#3758) --- esphome/dashboard/dashboard.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 1fadac968d..21730033a5 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -816,14 +816,17 @@ class LoginHandler(BaseHandler): import requests headers = { - "Authentication": f"Bearer {os.getenv('SUPERVISOR_TOKEN')}", + "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), } + data = { "username": self.get_argument("username", ""), "password": self.get_argument("password", ""), } try: - req = requests.post("http://supervisor/auth", headers=headers, data=data) + req = requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) if req.status_code == 200: self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") From 733a84df7515019c20cf660139737c3a4818966f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Sep 2022 16:50:17 +1200 Subject: [PATCH 022/106] Bump version to 2022.8.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7d916e30af..d65aea7159 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.8.2" +__version__ = "2022.8.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 9655362f238bacc6e24a4c5446fb345f4d8afe8d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 21 Sep 2022 15:34:02 +1200 Subject: [PATCH 023/106] Bump version to 2022.9.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 290e5e2994..41057f3d29 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.0b5" +__version__ = "2022.9.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 34bef2f2ca73f5e73796ea62671dccb97de48e60 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 22 Sep 2022 07:38:31 +1200 Subject: [PATCH 024/106] Revert "fix spi timing issues" (#3838) --- esphome/components/spi/spi.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 6c92321ac8..7f0b0f481a 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -195,11 +195,6 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - if (cs != nullptr) { - this->active_cs_ = cs; - this->active_cs_->digital_write(false); - } - #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { uint8_t data_mode = SPI_MODE0; @@ -220,6 +215,11 @@ class SPIComponent : public Component { #ifdef USE_SPI_ARDUINO_BACKEND } #endif // USE_SPI_ARDUINO_BACKEND + + if (cs != nullptr) { + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } } void disable(); From 66226abb4877de61261b4bc5553ea7caf5664a4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 22 Sep 2022 07:40:15 +1200 Subject: [PATCH 025/106] Bump version to 2022.9.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 41057f3d29..ce5a51aa71 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.0" +__version__ = "2022.9.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From efdb3d1f40eb0750b15ff2e3894aea0ac8ba28d3 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 25 Sep 2022 18:16:30 -0300 Subject: [PATCH 026/106] Bump dashboard to 20220925.0 (#3846) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 55544e442e..7f86acdbef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.0.2 # When updating platformio, also update Dockerfile esptool==3.3.1 click==8.1.3 -esphome-dashboard==20220920.1 +esphome-dashboard==20220925.0 aioesphomeapi==10.13.0 zeroconf==0.39.1 From d2c1c7507cbb42dd1bfa857006b3db9f6432f4d1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 29 Sep 2022 16:00:55 +1300 Subject: [PATCH 027/106] Bump version to 2022.9.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ce5a51aa71..a272f518df 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.1" +__version__ = "2022.9.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 1001d9c04e738d619c8a76796a55abc1a3549367 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 4 Oct 2022 10:45:06 +1300 Subject: [PATCH 028/106] Bluetooth Proxy active connections (#3817) --- CODEOWNERS | 1 + .../airthings_wave_mini.cpp | 6 +- .../airthings_wave_plus.cpp | 6 +- esphome/components/am43/am43.cpp | 10 +- esphome/components/am43/cover/am43_cover.cpp | 21 +- esphome/components/anova/anova.cpp | 22 +- esphome/components/api/api.proto | 177 +++- esphome/components/api/api_connection.cpp | 72 +- esphome/components/api/api_connection.h | 31 +- esphome/components/api/api_pb2.cpp | 764 +++++++++++++++++- esphome/components/api/api_pb2.h | 231 +++++- esphome/components/api/api_pb2_service.cpp | 285 ++++++- esphome/components/api/api_pb2_service.h | 95 ++- esphome/components/api/api_server.cpp | 45 ++ esphome/components/api/api_server.h | 6 + esphome/components/bedjet/bedjet_hub.cpp | 48 +- esphome/components/bedjet/bedjet_hub.h | 2 - esphome/components/ble_client/__init__.py | 7 +- esphome/components/ble_client/automation.cpp | 6 +- esphome/components/ble_client/automation.h | 3 +- esphome/components/ble_client/ble_client.cpp | 409 +--------- esphome/components/ble_client/ble_client.h | 86 +- .../components/ble_client/sensor/automation.h | 3 +- .../ble_client/sensor/ble_sensor.cpp | 12 +- .../ble_client/text_sensor/automation.h | 3 +- .../text_sensor/ble_text_sensor.cpp | 12 +- .../components/bluetooth_proxy/__init__.py | 16 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 323 +++++++- .../bluetooth_proxy/bluetooth_proxy.h | 39 +- .../components/esp32_ble_client/__init__.py | 12 + .../esp32_ble_client/ble_characteristic.cpp | 83 ++ .../esp32_ble_client/ble_characteristic.h | 35 + .../esp32_ble_client/ble_client_base.cpp | 324 ++++++++ .../esp32_ble_client/ble_client_base.h | 72 ++ .../esp32_ble_client/ble_descriptor.h | 25 + .../esp32_ble_client/ble_service.cpp | 66 ++ .../components/esp32_ble_client/ble_service.h | 32 + .../components/esp32_ble_tracker/__init__.py | 2 +- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 37 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 5 +- esphome/components/esp32_ble_tracker/queue.h | 10 +- .../display/pvvx_display.cpp | 5 +- .../radon_eye_rd200/radon_eye_rd200.cpp | 12 +- esphome/const.py | 1 + 44 files changed, 2818 insertions(+), 644 deletions(-) create mode 100644 esphome/components/esp32_ble_client/__init__.py create mode 100644 esphome/components/esp32_ble_client/ble_characteristic.cpp create mode 100644 esphome/components/esp32_ble_client/ble_characteristic.h create mode 100644 esphome/components/esp32_ble_client/ble_client_base.cpp create mode 100644 esphome/components/esp32_ble_client/ble_client_base.h create mode 100644 esphome/components/esp32_ble_client/ble_descriptor.h create mode 100644 esphome/components/esp32_ble_client/ble_service.cpp create mode 100644 esphome/components/esp32_ble_client/ble_service.h diff --git a/CODEOWNERS b/CODEOWNERS index 69e30027a9..d95ebcce59 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ esphome/components/ektf2232/* @jesserockz esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz +esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp index 2e7a1fb024..40873ec005 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -38,7 +38,7 @@ void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -88,8 +88,8 @@ void AirthingsWaveMini::update() { } void AirthingsWaveMini::request_read_values_() { - auto status = - esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, + ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); } diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index 02ed33b87a..11f86307fe 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -38,7 +38,7 @@ void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -109,8 +109,8 @@ void AirthingsWavePlus::update() { } void AirthingsWavePlus::request_read_values_() { - auto status = - esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, + ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); } diff --git a/esphome/components/am43/am43.cpp b/esphome/components/am43/am43.cpp index 1c4bad64c3..9a0e5999d2 100644 --- a/esphome/components/am43/am43.cpp +++ b/esphome/components/am43/am43.cpp @@ -76,9 +76,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i if (this->current_sensor_ > 0) { if (this->illuminance_ != nullptr) { auto *packet = this->encoder_->get_light_level_request(); - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, - packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), + this->char_handle_, packet->length, packet->data, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); @@ -102,8 +102,8 @@ void Am43::update() { if (this->battery_ != nullptr) { auto *packet = this->encoder_->get_battery_level_request(); auto status = - esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, - packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); } diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 39089e73c0..ba8e350732 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -27,8 +27,8 @@ void Am43Component::loop() { if (this->node_state == espbt::ClientState::ESTABLISHED && !this->logged_in_) { auto *packet = this->encoder_->get_send_pin_request(this->pin_); auto status = - esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, - packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str()); if (status) { ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status); @@ -54,8 +54,8 @@ void Am43Component::control(const CoverCall &call) { if (call.get_stop()) { auto *packet = this->encoder_->get_stop_request(); auto status = - esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, - packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); } @@ -66,8 +66,8 @@ void Am43Component::control(const CoverCall &call) { pos = 1 - pos; auto *packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); auto status = - esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, - packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status); } @@ -92,7 +92,8 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } this->char_handle_ = chr->handle; - auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); } @@ -122,9 +123,9 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->decoder_->pin_ok_) { ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str()); auto *packet = this->encoder_->get_position_request(); - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, - packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), + this->char_handle_, packet->length, packet->data, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status); } else { diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 6c8316d338..cafd30149d 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -34,15 +34,17 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported mode: %d", mode); return; } - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, - pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); } if (call.get_target_temperature().has_value()) { auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, - pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); } @@ -65,7 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } this->char_handle_ = chr->handle; - auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); } @@ -112,8 +115,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } if (pkt != nullptr) { auto status = - esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, - pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); @@ -137,8 +140,9 @@ void Anova::update() { auto *pkt = this->codec_->get_read_device_status_request(); if (this->current_request_ == 0) this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, - pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); this->current_request_++; diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c6cde8b038..9fa77d2daa 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -27,7 +27,6 @@ service APIConnection { rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} - rpc subscribe_bluetooth_le_advertisements (SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc get_time (GetTimeRequest) returns (GetTimeResponse) { option (needs_authentication) = false; } @@ -44,6 +43,16 @@ service APIConnection { rpc button_command (ButtonCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {} rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + + rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} + rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} + rpc bluetooth_gatt_get_services(BluetoothGATTGetServicesRequest) returns (void) {} + rpc bluetooth_gatt_read(BluetoothGATTReadRequest) returns (void) {} + rpc bluetooth_gatt_write(BluetoothGATTWriteRequest) returns (void) {} + rpc bluetooth_gatt_read_descriptor(BluetoothGATTReadDescriptorRequest) returns (void) {} + rpc bluetooth_gatt_write_descriptor(BluetoothGATTWriteDescriptorRequest) returns (void) {} + rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {} + rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} } @@ -78,6 +87,8 @@ message HelloRequest { // Not strictly necessary to send but nice for debugging // purposes. string client_info = 1; + uint32 api_version_major = 2; + uint32 api_version_minor = 3; } // Confirmation of successful connection request. @@ -192,7 +203,7 @@ message DeviceInfoResponse { uint32 webserver_port = 10; - bool has_bluetooth_proxy = 11; + uint32 bluetooth_proxy_version = 11; } message ListEntitiesRequest { @@ -1111,7 +1122,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { message BluetoothServiceData { string uuid = 1; - repeated uint32 data = 2 [packed=false]; + repeated uint32 legacy_data = 2 [deprecated = true]; + bytes data = 3; // Changed in proto version 1.7 } message BluetoothLEAdvertisementResponse { option (id) = 67; @@ -1127,3 +1139,162 @@ message BluetoothLEAdvertisementResponse { repeated BluetoothServiceData service_data = 5; repeated BluetoothServiceData manufacturer_data = 6; } + +enum BluetoothDeviceRequestType { + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; + BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; + BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; + BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; +} + +message BluetoothDeviceRequest { + option (id) = 68; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + BluetoothDeviceRequestType request_type = 2; +} + +message BluetoothDeviceConnectionResponse { + option (id) = 69; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool connected = 2; + uint32 mtu = 3; + int32 error = 4; +} + +message BluetoothGATTGetServicesRequest { + option (id) = 70; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; +} + +message BluetoothGATTDescriptor { + repeated uint64 uuid = 1; + uint32 handle = 2; +} + +message BluetoothGATTCharacteristic { + repeated uint64 uuid = 1; + uint32 handle = 2; + uint32 properties = 3; + repeated BluetoothGATTDescriptor descriptors = 4; +} + +message BluetoothGATTService { + repeated uint64 uuid = 1; + uint32 handle = 2; + repeated BluetoothGATTCharacteristic characteristics = 3; +} + +message BluetoothGATTGetServicesResponse { + option (id) = 71; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + repeated BluetoothGATTService services = 2; +} + +message BluetoothGATTGetServicesDoneResponse { + option (id) = 72; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; +} + +message BluetoothGATTReadRequest { + option (id) = 73; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTReadResponse { + option (id) = 74; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; + +} + +message BluetoothGATTWriteRequest { + option (id) = 75; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + bool response = 3; + + bytes data = 4; +} + +message BluetoothGATTReadDescriptorRequest { + option (id) = 76; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTWriteDescriptorRequest { + option (id) = 77; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; +} + +message BluetoothGATTNotifyRequest { + option (id) = 78; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + bool enable = 3; +} + +message BluetoothGATTNotifyDataResponse { + option (id) = 79; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; +} + +message SubscribeBluetoothConnectionsFreeRequest { + option (id) = 80; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; +} + +message BluetoothConnectionsFreeResponse { + option (id) = 81; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 free = 1; + uint32 limit = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a3b000c778..1dbf0abec4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,10 +1,10 @@ #include "api_connection.h" -#include "esphome/core/entity_base.h" -#include "esphome/core/log.h" -#include "esphome/components/network/util.h" -#include "esphome/core/version.h" -#include "esphome/core/hal.h" #include +#include "esphome/components/network/util.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/version.h" #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -12,6 +12,9 @@ #ifdef USE_HOMEASSISTANT_TIME #include "esphome/components/homeassistant/time/homeassistant_time.h" #endif +#ifdef USE_BLUETOOTH_PROXY +#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" +#endif namespace esphome { namespace api { @@ -823,6 +826,56 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { } #endif +#ifdef USE_BLUETOOTH_PROXY +bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { + if (!this->bluetooth_le_advertisement_subscription_) + return false; + if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { + BluetoothLEAdvertisementResponse resp = msg; + for (auto &service : resp.service_data) { + service.legacy_data.assign(service.data.begin(), service.data.end()); + service.data.clear(); + } + for (auto &manufacturer_data : resp.manufacturer_data) { + manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); + manufacturer_data.data.clear(); + } + return this->send_bluetooth_le_advertisement_response(resp); + } + return this->send_bluetooth_le_advertisement_response(msg); +} +void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); +} +void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg); +} +void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg); +} +void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg); +} +void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg); +} +void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg); +} + +void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); +} + +BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_free( + const SubscribeBluetoothConnectionsFreeRequest &msg) { + BluetoothConnectionsFreeResponse resp; + resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); + resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); + return resp; +} +#endif + bool APIConnection::send_log_message(int level, const char *tag, const char *line) { if (this->log_subscription_ < level) return false; @@ -840,11 +893,14 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin HelloResponse APIConnection::hello(const HelloRequest &msg) { this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; this->helper_->set_log_info(client_info_); - ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); + this->client_api_version_major_ = msg.api_version_major; + this->client_api_version_minor_ = msg.api_version_minor; + ESP_LOGV(TAG, "Hello from client: '%s' | API Version %d.%d", this->client_info_.c_str(), + this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 6; + resp.api_version_minor = 7; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); @@ -888,7 +944,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.has_bluetooth_proxy = true; + resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() ? 2 : 1; #endif return resp; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index dcf8bacad2..028fb80175 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -1,15 +1,11 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/application.h" +#include "api_frame_helper.h" #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" -#include "api_frame_helper.h" - -#ifdef USE_BLUETOOTH_PROXY -#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" -#endif +#include "esphome/core/application.h" +#include "esphome/core/component.h" namespace esphome { namespace api { @@ -99,11 +95,18 @@ class APIConnection : public APIServerConnection { this->send_homeassistant_service_response(call); } #ifdef USE_BLUETOOTH_PROXY - bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { - if (!this->bluetooth_le_advertisement_subscription_) - return false; - return this->send_bluetooth_le_advertisement_response(call); - } + bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); + + void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; + void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; + void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override; + void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override; + void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; + void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; + void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; + BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( + const SubscribeBluetoothConnectionsFreeRequest &msg) override; + #endif #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { @@ -181,6 +184,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr helper_; std::string client_info_; + uint32_t client_api_version_major_{0}; + uint32_t client_api_version_minor_{0}; #ifdef USE_ESP32_CAMERA esp32_camera::CameraImageReader image_reader_; #endif @@ -190,7 +195,7 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; - bool bluetooth_le_advertisement_subscription_{true}; + bool bluetooth_le_advertisement_subscription_{false}; bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 13969fce76..73d8044cde 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -340,6 +340,35 @@ template<> const char *proto_enum_to_string(enums::Me return "UNKNOWN"; } } +template<> +const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { + switch (value) { + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + default: + return "UNKNOWN"; + } +} +bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->api_version_major = value.as_uint32(); + return true; + } + case 3: { + this->api_version_minor = value.as_uint32(); + return true; + } + default: + return false; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -350,7 +379,11 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) return false; } } -void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); } +void HelloRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->client_info); + buffer.encode_uint32(2, this->api_version_major); + buffer.encode_uint32(3, this->api_version_minor); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HelloRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -358,6 +391,16 @@ void HelloRequest::dump_to(std::string &out) const { out.append(" client_info: "); out.append("'").append(this->client_info).append("'"); out.append("\n"); + + out.append(" api_version_major: "); + sprintf(buffer, "%u", this->api_version_major); + out.append(buffer); + out.append("\n"); + + out.append(" api_version_minor: "); + sprintf(buffer, "%u", this->api_version_minor); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -496,7 +539,7 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 11: { - this->has_bluetooth_proxy = value.as_bool(); + this->bluetooth_proxy_version = value.as_uint32(); return true; } default: @@ -548,7 +591,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); buffer.encode_uint32(10, this->webserver_port); - buffer.encode_bool(11, this->has_bluetooth_proxy); + buffer.encode_uint32(11, this->bluetooth_proxy_version); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -595,8 +638,9 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" has_bluetooth_proxy: "); - out.append(YESNO(this->has_bluetooth_proxy)); + out.append(" bluetooth_proxy_version: "); + sprintf(buffer, "%u", this->bluetooth_proxy_version); + out.append(buffer); out.append("\n"); out.append("}"); } @@ -4872,7 +4916,7 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->data.push_back(value.as_uint32()); + this->legacy_data.push_back(value.as_uint32()); return true; } default: @@ -4885,15 +4929,20 @@ bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited this->uuid = value.as_string(); return true; } + case 3: { + this->data = value.as_string(); + return true; + } default: return false; } } void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->uuid); - for (auto &it : this->data) { + for (auto &it : this->legacy_data) { buffer.encode_uint32(2, it, true); } + buffer.encode_string(3, this->data); } #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothServiceData::dump_to(std::string &out) const { @@ -4903,12 +4952,16 @@ void BluetoothServiceData::dump_to(std::string &out) const { out.append("'").append(this->uuid).append("'"); out.append("\n"); - for (const auto &it : this->data) { - out.append(" data: "); + for (const auto &it : this->legacy_data) { + out.append(" legacy_data: "); sprintf(buffer, "%u", it); out.append(buffer); out.append("\n"); } + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -5000,6 +5053,699 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->request_type = value.as_enum(); + return true; + } + default: + return false; + } +} +void BluetoothDeviceRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_enum(2, this->request_type); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothDeviceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" request_type: "); + out.append(proto_enum_to_string(this->request_type)); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothDeviceConnectionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->connected = value.as_bool(); + return true; + } + case 3: { + this->mtu = value.as_uint32(); + return true; + } + case 4: { + this->error = value.as_int32(); + return true; + } + default: + return false; + } +} +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_bool(2, this->connected); + buffer.encode_uint32(3, this->mtu); + buffer.encode_int32(4, this->error); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceConnectionResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" connected: "); + out.append(YESNO(this->connected)); + out.append("\n"); + + out.append(" mtu: "); + sprintf(buffer, "%u", this->mtu); + out.append(buffer); + out.append("\n"); + + out.append(" error: "); + sprintf(buffer, "%d", this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + default: + return false; + } +} +void BluetoothGATTGetServicesRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTDescriptor::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->uuid.push_back(value.as_uint64()); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->uuid) { + buffer.encode_uint64(1, it, true); + } + buffer.encode_uint32(2, this->handle); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTDescriptor::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTDescriptor {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + sprintf(buffer, "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->uuid.push_back(value.as_uint64()); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + case 3: { + this->properties = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTCharacteristic::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->descriptors.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->uuid) { + buffer.encode_uint64(1, it, true); + } + buffer.encode_uint32(2, this->handle); + buffer.encode_uint32(3, this->properties); + for (auto &it : this->descriptors) { + buffer.encode_message(4, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTCharacteristic::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTCharacteristic {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + sprintf(buffer, "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" properties: "); + sprintf(buffer, "%u", this->properties); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->descriptors) { + out.append(" descriptors: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif +bool BluetoothGATTService::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->uuid.push_back(value.as_uint64()); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTService::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->characteristics.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->uuid) { + buffer.encode_uint64(1, it, true); + } + buffer.encode_uint32(2, this->handle); + for (auto &it : this->characteristics) { + buffer.encode_message(3, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTService::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTService {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + sprintf(buffer, "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->characteristics) { + out.append(" characteristics: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif +bool BluetoothGATTGetServicesResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTGetServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->services.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + for (auto &it : this->services) { + buffer.encode_message(2, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->services) { + out.append(" services: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif +bool BluetoothGATTGetServicesDoneResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + default: + return false; + } +} +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesDoneResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTReadRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTReadRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTReadResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTReadResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_string(3, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTReadResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + case 3: { + this->response = value.as_bool(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothGATTWriteRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_bool(3, this->response); + buffer.encode_string(4, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTWriteRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" response: "); + out.append(YESNO(this->response)); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTReadDescriptorRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadDescriptorRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothGATTWriteDescriptorRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_string(3, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteDescriptorRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + case 3: { + this->enable = value.as_bool(); + return true; + } + default: + return false; + } +} +void BluetoothGATTNotifyRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_bool(3, this->enable); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyRequest {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" enable: "); + out.append(YESNO(this->enable)); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTNotifyDataResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothGATTNotifyDataResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_string(3, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyDataResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +void SubscribeBluetoothConnectionsFreeRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { + out.append("SubscribeBluetoothConnectionsFreeRequest {}"); +} +#endif +bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->free = value.as_uint32(); + return true; + } + case 2: { + this->limit = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->free); + buffer.encode_uint32(2, this->limit); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothConnectionsFreeResponse {\n"); + out.append(" free: "); + sprintf(buffer, "%u", this->free); + out.append(buffer); + out.append("\n"); + + out.append(" limit: "); + sprintf(buffer, "%u", this->limit); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2093f93ee7..325e9a23c3 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -155,12 +155,20 @@ enum MediaPlayerCommand : uint32_t { MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_UNMUTE = 4, }; +enum BluetoothDeviceRequestType : uint32_t { + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, + BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, + BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2, + BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3, +}; } // namespace enums class HelloRequest : public ProtoMessage { public: std::string client_info{}; + uint32_t api_version_major{0}; + uint32_t api_version_minor{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -168,6 +176,7 @@ class HelloRequest : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class HelloResponse : public ProtoMessage { public: @@ -263,7 +272,7 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; uint32_t webserver_port{0}; - bool has_bluetooth_proxy{false}; + uint32_t bluetooth_proxy_version{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1227,7 +1236,8 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { class BluetoothServiceData : public ProtoMessage { public: std::string uuid{}; - std::vector data{}; + std::vector legacy_data{}; + std::string data{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1254,6 +1264,223 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothDeviceRequest : public ProtoMessage { + public: + uint64_t address{0}; + enums::BluetoothDeviceRequestType request_type{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothDeviceConnectionResponse : public ProtoMessage { + public: + uint64_t address{0}; + bool connected{false}; + uint32_t mtu{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTGetServicesRequest : public ProtoMessage { + public: + uint64_t address{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTDescriptor : public ProtoMessage { + public: + std::vector uuid{}; + uint32_t handle{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTCharacteristic : public ProtoMessage { + public: + std::vector uuid{}; + uint32_t handle{0}; + uint32_t properties{0}; + std::vector descriptors{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTService : public ProtoMessage { + public: + std::vector uuid{}; + uint32_t handle{0}; + std::vector characteristics{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTGetServicesResponse : public ProtoMessage { + public: + uint64_t address{0}; + std::vector services{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { + public: + uint64_t address{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTReadRequest : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTReadResponse : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTWriteRequest : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + bool response{false}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTReadDescriptorRequest : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTNotifyRequest : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + bool enable{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothGATTNotifyDataResponse : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class BluetoothConnectionsFreeResponse : public ProtoMessage { + public: + uint32_t free{0}; + uint32_t limit{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6932675c41..7bfe1acf48 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -336,6 +336,71 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu return this->send_message_(msg, 67); } #endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_device_connection_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 69); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_get_services_response(const BluetoothGATTGetServicesResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_get_services_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 71); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_get_services_done_response( + const BluetoothGATTGetServicesDoneResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_get_services_done_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 72); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_read_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 74); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_notify_data_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 79); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_connections_free_response(const BluetoothConnectionsFreeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_connections_free_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 81); +} +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -612,6 +677,94 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_bluetooth_le_advertisements_request(msg); break; } + case 68: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothDeviceRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_device_request(msg); +#endif + break; + } + case 70: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTGetServicesRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_get_services_request(msg); +#endif + break; + } + case 73: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTReadRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_read_request(msg); +#endif + break; + } + case 75: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_write_request(msg); +#endif + break; + } + case 76: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTReadDescriptorRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_read_descriptor_request(msg); +#endif + break; + } + case 77: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTWriteDescriptorRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_write_descriptor_request(msg); +#endif + break; + } + case 78: { +#ifdef USE_BLUETOOTH_PROXY + BluetoothGATTNotifyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str()); +#endif + this->on_bluetooth_gatt_notify_request(msg); +#endif + break; + } + case 80: { +#ifdef USE_BLUETOOTH_PROXY + SubscribeBluetoothConnectionsFreeRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); +#endif + this->on_subscribe_bluetooth_connections_free_request(msg); +#endif + break; + } default: return false; } @@ -708,18 +861,6 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc } this->subscribe_home_assistant_states(msg); } -void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_bluetooth_le_advertisements(msg); -} void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { if (!this->is_connection_setup()) { this->on_no_setup_connection(); @@ -884,6 +1025,126 @@ void APIServerConnection::on_media_player_command_request(const MediaPlayerComma this->media_player_command(msg); } #endif +void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( + const SubscribeBluetoothLEAdvertisementsRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_bluetooth_le_advertisements(msg); +} +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_device_request(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_get_services(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_read(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_write(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_read_descriptor(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_write_descriptor(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->bluetooth_gatt_notify(msg); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void APIServerConnection::on_subscribe_bluetooth_connections_free_request( + const SubscribeBluetoothConnectionsFreeRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); + if (!this->send_bluetooth_connections_free_response(ret)) { + this->on_fatal_error(); + } +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 49426d1bbc..c7ef1468d8 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -158,6 +158,48 @@ class APIServerConnectionBase : public ProtoService { const SubscribeBluetoothLEAdvertisementsRequest &value){}; #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_get_services_response(const BluetoothGATTGetServicesResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_get_services_done_response(const BluetoothGATTGetServicesDoneResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_connections_free_response(const BluetoothConnectionsFreeResponse &msg); #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -175,7 +217,6 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; - virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #ifdef USE_COVER @@ -210,6 +251,32 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_MEDIA_PLAYER virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; +#endif + virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( + const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -222,7 +289,6 @@ class APIServerConnection : public APIServerConnectionBase { void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override; #ifdef USE_COVER @@ -257,6 +323,31 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_MEDIA_PLAYER void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; +#endif + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; +#endif +#ifdef USE_BLUETOOTH_PROXY + void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override; #endif }; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5851594955..965f08aa15 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -297,6 +297,51 @@ void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementRe client->send_bluetooth_le_advertisement(call); } } +void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { + BluetoothDeviceConnectionResponse call; + call.address = address; + call.connected = connected; + call.mtu = mtu; + call.error = error; + + for (auto &client : this->clients_) { + client->send_bluetooth_device_connection_response(call); + } +} + +void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { + BluetoothConnectionsFreeResponse call; + call.free = free; + call.limit = limit; + + for (auto &client : this->clients_) { + client->send_bluetooth_connections_free_response(call); + } +} + +void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_read_response(call); + } +} +void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_notify_data_response(call); + } +} +void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_get_services_response(call); + } +} +void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { + BluetoothGATTGetServicesDoneResponse call; + call.address = address; + + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_get_services_done_response(call); + } +} #endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index dc892b2088..72ab55432c 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -75,6 +75,12 @@ class APIServer : public Component, public Controller { void send_homeassistant_service_call(const HomeassistantServiceResponse &call); #ifdef USE_BLUETOOTH_PROXY void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); + void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error = ESP_OK); + void send_bluetooth_connections_free(uint8_t free, uint8_t limit); + void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); + void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); + void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); + void send_bluetooth_gatt_services_done(uint64_t address); #endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fd383eb6be..f90ca5cf54 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -128,9 +128,9 @@ uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) { } return -1; } - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, - pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), + this->char_handle_cmd_, pkt->data_length + 1, (uint8_t *) &pkt->command, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); return status; } @@ -138,13 +138,13 @@ uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) { uint8_t BedJetHub::set_notify_(const bool enable) { uint8_t status; if (enable) { - status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), this->char_handle_status_); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); } } else { - status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + status = esp_ble_gattc_unregister_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), this->char_handle_status_); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); @@ -204,8 +204,8 @@ bool BedJetHub::discover_characteristics_() { result = false; } else { this->char_handle_name_ = chr->handle; - auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), + this->char_handle_name_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); } @@ -230,22 +230,6 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->dispatch_state_(false); break; } - case ESP_GATTC_OPEN_EVT: { - // FIXME: bug in BLEClient - this->parent_->conn_id = param->open.conn_id; - this->open_conn_id_ = param->open.conn_id; - break; - } - - case ESP_GATTC_CONNECT_EVT: { - if (this->parent_->conn_id != param->connect.conn_id && this->open_conn_id_ != 0xff) { - // FIXME: bug in BLEClient - ESP_LOGW(TAG, "[%s] CONNECT_EVT unexpected conn_id; open=%d, parent=%d, param=%d", this->get_name().c_str(), - this->open_conn_id_, this->parent_->conn_id, param->connect.conn_id); - this->parent_->conn_id = this->open_conn_id_; - } - break; - } case ESP_GATTC_SEARCH_CMPL_EVT: { auto result = this->discover_characteristics_(); @@ -301,7 +285,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent_->conn_id) + if (param->read.conn_id != this->parent_->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -358,9 +342,9 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga if (this->processing_) break; - if (param->notify.conn_id != this->parent_->conn_id) { + if (param->notify.conn_id != this->parent_->get_conn_id()) { ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", - this->get_name().c_str(), this->parent_->conn_id, param->notify.conn_id); + this->get_name().c_str(), this->parent_->get_conn_id(), param->notify.conn_id); // FIXME: bug in BLEClient holding wrong conn_id. } @@ -394,7 +378,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga if (needs_extra) { // This means the packet was partial, so read the status characteristic to get the second part. // Ideally this will complete quickly. We won't process additional notification events until it does. - auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, + auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); @@ -438,15 +422,15 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. uint16_t notify_en = enable ? 1 : 0; - auto status = - esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), - (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char_descr(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), handle, + sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, + ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); return status; } ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(), - enable ? "true" : "false", handle, this->parent_->conn_id); + enable ? "true" : "false", handle, this->parent_->get_conn_id()); return ESP_GATT_OK; } @@ -500,7 +484,7 @@ void BedJetHub::update() { this->dispatch_status_(); } void BedJetHub::dump_config() { ESP_LOGCONFIG(TAG, "BedJet Hub '%s'", this->get_name().c_str()); ESP_LOGCONFIG(TAG, " ble_client.app_id: %d", this->parent()->app_id); - ESP_LOGCONFIG(TAG, " ble_client.conn_id: %d", this->parent()->conn_id); + ESP_LOGCONFIG(TAG, " ble_client.conn_id: %d", this->parent()->get_conn_id()); LOG_UPDATE_INTERVAL(this) ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size()); for (auto *child : this->children_) { diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index c7583b78e9..f1479710a7 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -167,8 +167,6 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo uint16_t char_handle_status_; uint16_t config_descr_status_; - uint8_t open_conn_id_ = -1; - uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 4b7d5f5b8a..0f1f60e62b 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import esp32_ble_tracker +from esphome.components import esp32_ble_tracker, esp32_ble_client from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, @@ -14,13 +14,12 @@ from esphome.const import ( ) from esphome import automation +AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix"] DEPENDENCIES = ["esp32_ble_tracker"] ble_client_ns = cg.esphome_ns.namespace("ble_client") -BLEClient = ble_client_ns.class_( - "BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient -) +BLEClient = ble_client_ns.class_("BLEClient", esp32_ble_client.BLEClientBase) BLEClientNode = ble_client_ns.class_("BLEClientNode") BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const") # Triggers diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 6918ab31b4..395950dd26 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -1,8 +1,8 @@ #include "automation.h" +#include #include #include -#include #include "esphome/core/log.h" @@ -31,8 +31,8 @@ void BLEWriterClientNode::write(const std::vector &value) { } ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); esp_err_t err = - esp_ble_gattc_write_char(this->parent()->gattc_if, this->parent()->conn_id, this->ble_char_handle_, value.size(), - const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_, + value.size(), const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); } diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 38e64ebd76..ba5f78109e 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -28,7 +28,8 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { - if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0) + if (event == ESP_GATTC_DISCONNECT_EVT && + memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0) this->trigger(); if (event == ESP_GATTC_SEARCH_CMPL_EVT) this->node_state = espbt::ClientState::ESTABLISHED; diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5f58d8273f..3f86df32f5 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -1,8 +1,9 @@ -#include "esphome/core/log.h" +#include "ble_client.h" +#include "esphome/components/esp32_ble_client/ble_client_base.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include "ble_client.h" +#include "esphome/core/log.h" #ifdef USE_ESP32 @@ -11,22 +12,13 @@ namespace ble_client { static const char *const TAG = "ble_client"; -float BLEClient::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } - void BLEClient::setup() { - auto ret = esp_ble_gattc_app_register(this->app_id); - if (ret) { - ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); - this->mark_failed(); - } - this->set_states_(espbt::ClientState::IDLE); + BLEClientBase::setup(); this->enabled = true; } void BLEClient::loop() { - if (this->state() == espbt::ClientState::DISCOVERED) { - this->connect(); - } + BLEClientBase::loop(); for (auto *node : this->nodes_) node->loop(); } @@ -39,34 +31,7 @@ void BLEClient::dump_config() { bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { if (!this->enabled) return false; - if (device.address_uint64() != this->address) - return false; - if (this->state() != espbt::ClientState::IDLE) - return false; - - ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); - this->set_states_(espbt::ClientState::DISCOVERED); - - auto addr = device.address_uint64(); - this->remote_bda[0] = (addr >> 40) & 0xFF; - this->remote_bda[1] = (addr >> 32) & 0xFF; - this->remote_bda[2] = (addr >> 24) & 0xFF; - this->remote_bda[3] = (addr >> 16) & 0xFF; - this->remote_bda[4] = (addr >> 8) & 0xFF; - this->remote_bda[5] = (addr >> 0) & 0xFF; - this->remote_addr_type = device.get_address_type(); - return true; -} - -std::string BLEClient::address_str() const { - char buf[20]; - sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff, - (uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff, - (uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff, - (uint8_t)(this->address >> 0) & 0xff); - std::string ret; - ret = buf; - return ret; + return BLEClientBase::parse_device(device); } void BLEClient::set_enabled(bool enabled) { @@ -74,7 +39,7 @@ void BLEClient::set_enabled(bool enabled) { return; if (!enabled && this->state() != espbt::ClientState::IDLE) { ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); - auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id); + auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); if (ret) { ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); } @@ -82,125 +47,12 @@ void BLEClient::set_enabled(bool enabled) { this->enabled = enabled; } -void BLEClient::connect() { - ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); - auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, this->remote_addr_type, true); - if (ret) { - ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); - this->set_states_(espbt::ClientState::IDLE); - } else { - this->set_states_(espbt::ClientState::CONNECTING); - } -} - void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { - if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) - return; - if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if) - return; - bool all_established = this->all_nodes_established_(); - switch (event) { - case ESP_GATTC_REG_EVT: { - if (param->reg.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "gattc registered app id %d", this->app_id); - this->gattc_if = esp_gattc_if; - } else { - ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); - } - break; - } - case ESP_GATTC_OPEN_EVT: { - ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); - this->conn_id = param->open.conn_id; - if (param->open.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); - this->set_states_(espbt::ClientState::IDLE); - break; - } - break; - } - case ESP_GATTC_CONNECT_EVT: { - ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str()); - if (this->conn_id != param->connect.conn_id) { - ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d", - this->address_str().c_str(), param->connect.conn_id, this->conn_id); - } - auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id); - if (ret) { - ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret); - } - break; - } - case ESP_GATTC_CFG_MTU_EVT: { - if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu, - param->cfg_mtu.status); - this->set_states_(espbt::ClientState::IDLE); - break; - } - ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); - esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); - break; - } - case ESP_GATTC_DISCONNECT_EVT: { - if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { - return; - } - ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason); - for (auto &svc : this->services_) - delete svc; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.clear(); - this->set_states_(espbt::ClientState::IDLE); - break; - } - case ESP_GATTC_SEARCH_RES_EVT: { - BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) - ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); - ble_service->start_handle = param->search_res.start_handle; - ble_service->end_handle = param->search_res.end_handle; - ble_service->client = this; - this->services_.push_back(ble_service); - break; - } - case ESP_GATTC_SEARCH_CMPL_EVT: { - ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); - for (auto &svc : this->services_) { - ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); - ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); - svc->parse_characteristics(); - } - this->set_states_(espbt::ClientState::CONNECTED); - this->set_state(espbt::ClientState::ESTABLISHED); - break; - } - case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - auto *descr = this->get_config_descriptor(param->reg_for_notify.handle); - if (descr == nullptr) { - ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); - break; - } - if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || - descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { - ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, - descr->uuid.to_string().c_str()); - break; - } - uint16_t notify_en = 1; - auto status = - esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), - (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); - } - break; - } + BLEClientBase::gattc_event_handler(event, esp_gattc_if, param); - default: - break; - } for (auto *node : this->nodes_) node->gattc_event_handler(event, esp_gattc_if, param); @@ -212,237 +64,20 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } } -void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - switch (event) { - // This event is sent by the server when it requests security - case ESP_GAP_BLE_SEC_REQ_EVT: - ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event); - esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); - break; - // This event is sent once authentication has completed - case ESP_GAP_BLE_AUTH_CMPL_EVT: - esp_bd_addr_t bd_addr; - memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str()); - if (!param->ble_security.auth_cmpl.success) { - ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason); - } else { - ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type, - param->ble_security.auth_cmpl.auth_mode); - } - break; - // There are other events we'll want to implement at some point to support things like pass key - // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md - default: - break; +void BLEClient::set_state(espbt::ClientState state) { + BLEClientBase::set_state(state); + for (auto &node : nodes_) + node->node_state = state; +} + +bool BLEClient::all_nodes_established_() { + if (this->state() != espbt::ClientState::ESTABLISHED) + return false; + for (auto &node : nodes_) { + if (node->node_state != espbt::ClientState::ESTABLISHED) + return false; } -} - -// Parse GATT values into a float for a sensor. -// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ -float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { - // A length of one means a single octet value. - if (length == 0) - return 0; - if (length == 1) - return (float) ((uint8_t) value[0]); - - switch (value[0]) { - case 0x1: // boolean. - case 0x2: // 2bit. - case 0x3: // nibble. - case 0x4: // uint8. - return (float) ((uint8_t) value[1]); - case 0x5: // uint12. - case 0x6: // uint16. - if (length > 2) { - return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); - } - case 0x7: // uint24. - if (length > 3) { - return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); - } - case 0x8: // uint32. - if (length > 4) { - return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + - (uint32_t)(value[4])); - } - case 0xC: // int8. - return (float) ((int8_t) value[1]); - case 0xD: // int12. - case 0xE: // int16. - if (length > 2) { - return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); - } - case 0xF: // int24. - if (length > 3) { - return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); - } - case 0x10: // int32. - if (length > 4) { - return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + - (int32_t)(value[4])); - } - } - ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length); - return NAN; -} - -BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) { - for (auto *svc : this->services_) { - if (svc->uuid == uuid) - return svc; - } - return nullptr; -} - -BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } - -BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { - auto *svc = this->get_service(service); - if (svc == nullptr) - return nullptr; - return svc->get_characteristic(chr); -} - -BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) { - return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); -} - -BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) { - for (auto &svc : this->services_) { - for (auto &chr : svc->characteristics) { - if (chr->handle == handle) { - for (auto &desc : chr->descriptors) { - if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) - return desc; - } - } - } - } - return nullptr; -} - -BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { - for (auto &chr : this->characteristics) { - if (chr->uuid == uuid) - return chr; - } - return nullptr; -} - -BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { - return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); -} - -BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { - auto *svc = this->get_service(service); - if (svc == nullptr) - return nullptr; - auto *ch = svc->get_characteristic(chr); - if (ch == nullptr) - return nullptr; - return ch->get_descriptor(descr); -} - -BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { - return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), - espbt::ESPBTUUID::from_uint16(descr)); -} - -BLEService::~BLEService() { - for (auto &chr : this->characteristics) - delete chr; // NOLINT(cppcoreguidelines-owning-memory) -} - -void BLEService::parse_characteristics() { - uint16_t offset = 0; - esp_gattc_char_elem_t result; - - while (true) { - uint16_t count = 1; - esp_gatt_status_t status = esp_ble_gattc_get_all_char( - this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset); - if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { - break; - } - if (status != ESP_GATT_OK) { - ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); - break; - } - if (count == 0) { - break; - } - - BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory) - characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); - characteristic->properties = result.properties; - characteristic->handle = result.char_handle; - characteristic->service = this; - this->characteristics.push_back(characteristic); - ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), - characteristic->handle, characteristic->properties); - characteristic->parse_descriptors(); - offset++; - } -} - -BLECharacteristic::~BLECharacteristic() { - for (auto &desc : this->descriptors) - delete desc; // NOLINT(cppcoreguidelines-owning-memory) -} - -void BLECharacteristic::parse_descriptors() { - uint16_t offset = 0; - esp_gattc_descr_elem_t result; - - while (true) { - uint16_t count = 1; - esp_gatt_status_t status = esp_ble_gattc_get_all_descr( - this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset); - if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { - break; - } - if (status != ESP_GATT_OK) { - ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); - break; - } - if (count == 0) { - break; - } - - BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) - desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); - desc->handle = result.handle; - desc->characteristic = this; - this->descriptors.push_back(desc); - ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); - offset++; - } -} - -BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { - for (auto &desc : this->descriptors) { - if (desc->uuid == uuid) - return desc; - } - return nullptr; -} -BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { - return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); -} - -void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { - auto *client = this->service->client; - auto status = esp_ble_gattc_write_char(client->gattc_if, client->conn_id, this->handle, new_val_size, new_val, - write_type, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status); - } -} - -void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { - write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); + return true; } } // namespace ble_client diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 5ed8f219d1..0f8373ab1f 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -1,8 +1,9 @@ #pragma once +#include "esphome/components/esp32_ble_client/ble_client_base.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef USE_ESP32 @@ -18,9 +19,9 @@ namespace ble_client { namespace espbt = esphome::esp32_ble_tracker; +using namespace esp32_ble_client; + class BLEClient; -class BLEService; -class BLECharacteristic; class BLEClientNode { public: @@ -42,57 +43,15 @@ class BLEClientNode { uint64_t address_; }; -class BLEDescriptor { - public: - espbt::ESPBTUUID uuid; - uint16_t handle; - - BLECharacteristic *characteristic; -}; - -class BLECharacteristic { - public: - ~BLECharacteristic(); - espbt::ESPBTUUID uuid; - uint16_t handle; - esp_gatt_char_prop_t properties; - std::vector descriptors; - void parse_descriptors(); - BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); - BLEDescriptor *get_descriptor(uint16_t uuid); - void write_value(uint8_t *new_val, int16_t new_val_size); - void write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); - BLEService *service; -}; - -class BLEService { - public: - ~BLEService(); - espbt::ESPBTUUID uuid; - uint16_t start_handle; - uint16_t end_handle; - std::vector characteristics; - BLEClient *client; - void parse_characteristics(); - BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); - BLECharacteristic *get_characteristic(uint16_t uuid); -}; - -class BLEClient : public espbt::ESPBTClient, public Component { +class BLEClient : public BLEClientBase { public: void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; bool parse_device(const espbt::ESPBTDevice &device) override; - void on_scan_end() override {} - void connect() override; - - void set_address(uint64_t address) { this->address = address; } void set_enabled(bool enabled); @@ -102,43 +61,14 @@ class BLEClient : public espbt::ESPBTClient, public Component { this->nodes_.push_back(node); } - BLEService *get_service(espbt::ESPBTUUID uuid); - BLEService *get_service(uint16_t uuid); - BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); - BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); - BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); - BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); - // Get the configuration descriptor for the given characteristic handle. - BLEDescriptor *get_config_descriptor(uint16_t handle); - - float parse_char_value(uint8_t *value, uint16_t length); - - int gattc_if; - esp_bd_addr_t remote_bda; - esp_ble_addr_type_t remote_addr_type; - uint16_t conn_id; - uint64_t address; bool enabled; - std::string address_str() const; + + void set_state(espbt::ClientState state) override; protected: - void set_states_(espbt::ClientState st) { - this->set_state(st); - for (auto &node : nodes_) - node->node_state = st; - } - bool all_nodes_established_() { - if (this->state() != espbt::ClientState::ESTABLISHED) - return false; - for (auto &node : nodes_) { - if (node->node_state != espbt::ClientState::ESTABLISHED) - return false; - } - return true; - } + bool all_nodes_established_(); std::vector nodes_; - std::vector services_; }; } // namespace ble_client diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index 2baaafe2ec..d830165d30 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -19,7 +19,8 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || + param->notify.handle != this->sensor_->handle) break; this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index ee5afd3f7b..a05efad60b 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -63,8 +63,8 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->handle = descr->handle; } if (this->notify_) { - auto status = - esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), + this->parent()->get_remote_bda(), chr->handle); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); } @@ -74,7 +74,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -87,7 +87,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); @@ -122,8 +122,8 @@ void BLESensor::update() { return; } - auto status = - esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle, + ESP_GATT_AUTH_REQ_NONE); if (status) { this->status_set_warning(); this->publish_state(NAN); diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h index be85892c5a..c504c35a58 100644 --- a/esphome/components/ble_client/text_sensor/automation.h +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -19,7 +19,8 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || + param->notify.handle != this->sensor_->handle) break; this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len)); } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 1a71cd6cd8..1a304593c7 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -66,8 +66,8 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->handle = descr->handle; } if (this->notify_) { - auto status = - esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), + this->parent()->get_remote_bda(), chr->handle); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); } @@ -77,7 +77,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -90,7 +90,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); @@ -121,8 +121,8 @@ void BLETextSensor::update() { return; } - auto status = - esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle, + ESP_GATT_AUTH_REQ_NONE); if (status) { this->status_set_warning(); this->publish_state(EMPTY); diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index a6a5a4391b..b5acea89dd 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -1,19 +1,23 @@ -from esphome.components import esp32_ble_tracker +from esphome.components import esp32_ble_tracker, esp32_ble_client import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import CONF_ACTIVE, CONF_ID -DEPENDENCIES = ["esp32", "esp32_ble_tracker"] +AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] +DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz"] bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") -BluetoothProxy = bluetooth_proxy_ns.class_("BluetoothProxy", cg.Component) +BluetoothProxy = bluetooth_proxy_ns.class_( + "BluetoothProxy", esp32_ble_client.BLEClientBase +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BluetoothProxy), + cv.Optional(CONF_ACTIVE, default=False): cv.boolean, } ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) @@ -22,6 +26,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await esp32_ble_tracker.register_ble_device(var, config) + cg.add(var.set_active(config[CONF_ACTIVE])) + + await esp32_ble_tracker.register_client(var, config) cg.add_define("USE_BLUETOOTH_PROXY") diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 41871295ce..96fee39f95 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -1,22 +1,39 @@ #include "bluetooth_proxy.h" -#ifdef USE_API -#include "esphome/components/api/api_pb2.h" -#include "esphome/components/api/api_server.h" -#endif // USE_API #include "esphome/core/log.h" #ifdef USE_ESP32 +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + namespace esphome { namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; +BluetoothProxy::BluetoothProxy() { + global_bluetooth_proxy = this; + this->address_ = 0; +} + bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), device.get_rssi()); this->send_api_packet_(device); + + this->address_type_map_[device.address_uint64()] = device.get_address_type(); + + if (this->address_ == 0) + return true; + + if (this->state_ == espbt::ClientState::DISCOVERED) { + ESP_LOGV(TAG, "Connecting to address %s", this->address_str().c_str()); + return true; + } + + BLEClientBase::parse_device(device); return true; } @@ -35,23 +52,309 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi for (auto &data : device.get_service_datas()) { api::BluetoothServiceData service_data; service_data.uuid = data.uuid.to_string(); - for (auto d : data.data) - service_data.data.push_back(d); - resp.service_data.push_back(service_data); + service_data.data.assign(data.data.begin(), data.data.end()); + resp.service_data.push_back(std::move(service_data)); } for (auto &data : device.get_manufacturer_datas()) { api::BluetoothServiceData manufacturer_data; manufacturer_data.uuid = data.uuid.to_string(); - for (auto d : data.data) - manufacturer_data.data.push_back(d); - resp.manufacturer_data.push_back(manufacturer_data); + manufacturer_data.data.assign(data.data.begin(), data.data.end()); + resp.manufacturer_data.push_back(std::move(manufacturer_data)); } api::global_api_server->send_bluetooth_le_advertisement(resp); #endif } +void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEClientBase::gattc_event_handler(event, gattc_if, param); + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { +#ifdef USE_API + api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, + param->disconnect.reason); + api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), + this->get_bluetooth_connections_limit()); +#endif + this->address_ = 0; + } + case ESP_GATTC_OPEN_EVT: { + if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { +#ifdef USE_API + api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, param->open.status); + +#endif + break; + } + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { +#ifdef USE_API + api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); + api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), + this->get_bluetooth_connections_limit()); +#endif + break; + } + case ESP_GATTC_READ_DESCR_EVT: + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->conn_id_) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char/descriptor at handle %d, status=%d", param->read.handle, param->read.status); + break; + } +#ifdef USE_API + api::BluetoothGATTReadResponse resp; + resp.address = this->address_; + resp.handle = param->read.handle; + resp.data.reserve(param->read.value_len); + for (uint16_t i = 0; i < param->read.value_len; i++) { + resp.data.push_back(param->read.value[i]); + } + api::global_api_server->send_bluetooth_gatt_read_response(resp); +#endif + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->conn_id_) + break; + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT: handle=0x%x", param->notify.handle); +#ifdef USE_API + api::BluetoothGATTNotifyDataResponse resp; + resp.address = this->address_; + resp.handle = param->notify.handle; + resp.data.reserve(param->notify.value_len); + for (uint16_t i = 0; i < param->notify.value_len; i++) { + resp.data.push_back(param->notify.value[i]); + } + api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); +#endif + break; + } + default: + break; + } +} + void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); } +void BluetoothProxy::loop() { + BLEClientBase::loop(); +#ifdef USE_API + if (this->state_ != espbt::ClientState::IDLE && !api::global_api_server->is_connected()) { + ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str()); + auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s err=%d", this->address_str().c_str(), err); + } + } + + if (this->send_service_ == this->services_.size()) { + this->send_service_ = -1; + api::global_api_server->send_bluetooth_gatt_services_done(this->address_); + } else if (this->send_service_ >= 0) { + auto &service = this->services_[this->send_service_]; + api::BluetoothGATTGetServicesResponse resp; + resp.address = this->address_; + api::BluetoothGATTService service_resp; + service_resp.uuid = {service->uuid.get_128bit_high(), service->uuid.get_128bit_low()}; + service_resp.handle = service->start_handle; + for (auto &characteristic : service->characteristics) { + api::BluetoothGATTCharacteristic characteristic_resp; + characteristic_resp.uuid = {characteristic->uuid.get_128bit_high(), characteristic->uuid.get_128bit_low()}; + characteristic_resp.handle = characteristic->handle; + characteristic_resp.properties = characteristic->properties; + for (auto &descriptor : characteristic->descriptors) { + api::BluetoothGATTDescriptor descriptor_resp; + descriptor_resp.uuid = {descriptor->uuid.get_128bit_high(), descriptor->uuid.get_128bit_low()}; + descriptor_resp.handle = descriptor->handle; + characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); + } + service_resp.characteristics.push_back(std::move(characteristic_resp)); + } + resp.services.push_back(std::move(service_resp)); + api::global_api_server->send_bluetooth_gatt_services(resp); + this->send_service_++; + } +#endif +} + +#ifdef USE_API +void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { + switch (msg.request_type) { + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { + this->address_ = msg.address; + if (this->address_type_map_.find(this->address_) != this->address_type_map_.end()) { + // Utilise the address type cache + this->remote_addr_type_ = this->address_type_map_[this->address_]; + } else { + this->remote_addr_type_ = BLE_ADDR_TYPE_PUBLIC; + } + this->remote_bda_[0] = (this->address_ >> 40) & 0xFF; + this->remote_bda_[1] = (this->address_ >> 32) & 0xFF; + this->remote_bda_[2] = (this->address_ >> 24) & 0xFF; + this->remote_bda_[3] = (this->address_ >> 16) & 0xFF; + this->remote_bda_[4] = (this->address_ >> 8) & 0xFF; + this->remote_bda_[5] = (this->address_ >> 0) & 0xFF; + this->set_state(espbt::ClientState::DISCOVERED); + esp_ble_gap_stop_scanning(); + break; + } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { + if (this->state() != espbt::ClientState::IDLE) { + ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str()); + auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s err=%d", this->address_str().c_str(), err); + } + } + break; + } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: + break; + } +} + +void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected."); + return; + } + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for read GATT characteristic request"); + return; + } + + auto *characteristic = this->get_characteristic(msg.handle); + if (characteristic == nullptr) { + ESP_LOGW(TAG, "Cannot read GATT characteristic, not found."); + return; + } + + ESP_LOGV(TAG, "Reading GATT characteristic %s", characteristic->uuid.to_string().c_str()); + + esp_err_t err = + esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, characteristic->handle, ESP_GATT_AUTH_REQ_NONE); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); + } +} + +void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected."); + return; + } + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for write GATT characteristic request"); + return; + } + + auto *characteristic = this->get_characteristic(msg.handle); + if (characteristic == nullptr) { + ESP_LOGW(TAG, "Cannot write GATT characteristic, not found."); + return; + } + + ESP_LOGV(TAG, "Writing GATT characteristic %s", characteristic->uuid.to_string().c_str()); + characteristic->write_value((uint8_t *) msg.data.data(), msg.data.size(), + msg.response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP); +} + +void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not connected."); + return; + } + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for read GATT characteristic descriptor request"); + return; + } + + auto *descriptor = this->get_descriptor(msg.handle); + if (descriptor == nullptr) { + ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not found."); + return; + } + + ESP_LOGV(TAG, "Reading GATT characteristic descriptor %s -> %s", descriptor->characteristic->uuid.to_string().c_str(), + descriptor->uuid.to_string().c_str()); + + esp_err_t err = + esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, ESP_GATT_AUTH_REQ_NONE); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); + } +} + +void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not connected."); + return; + } + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for write GATT characteristic descriptor request"); + return; + } + + auto *descriptor = this->get_descriptor(msg.handle); + if (descriptor == nullptr) { + ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not found."); + return; + } + + ESP_LOGV(TAG, "Writing GATT characteristic descriptor %s -> %s", descriptor->characteristic->uuid.to_string().c_str(), + descriptor->uuid.to_string().c_str()); + + esp_err_t err = + esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, msg.data.size(), + (uint8_t *) msg.data.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, err=%d", err); + } +} + +void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) { + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for service list request"); + return; + } + this->send_service_ = 0; +} + +void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { + if (this->address_ != msg.address) { + ESP_LOGW(TAG, "Address mismatch for notify"); + return; + } + + auto *characteristic = this->get_characteristic(msg.handle); + + if (characteristic == nullptr) { + ESP_LOGW(TAG, "Cannot notify GATT characteristic, not found."); + return; + } + + esp_err_t err; + if (msg.enable) { + err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, err=%d", err); + } + } else { + err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_unregister_for_notify failed, err=%d", err); + } + } +} + +#endif + +BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 9a936747c0..8ff43aff3f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -4,22 +4,59 @@ #include +#include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#include + +#ifdef USE_API +#include "esphome/components/api/api_pb2.h" +#endif // USE_API namespace esphome { namespace bluetooth_proxy { -class BluetoothProxy : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +using namespace esp32_ble_client; + +class BluetoothProxy : public BLEClientBase { public: + BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; + void loop() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + +#ifdef USE_API + void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); + void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg); + void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg); + void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg); + void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); + void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); + void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); +#endif + + int get_bluetooth_connections_free() { return this->state_ == espbt::ClientState::IDLE ? 1 : 0; } + int get_bluetooth_connections_limit() { return 1; } + + void set_active(bool active) { this->active_ = active; } + bool has_active() { return this->active_; } protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); + + std::map address_type_map_; + int16_t send_service_{-1}; + bool active_; }; +extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py new file mode 100644 index 0000000000..94a5576d0b --- /dev/null +++ b/esphome/components/esp32_ble_client/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg + +from esphome.components import esp32_ble_tracker + +AUTO_LOAD = ["esp32_ble_tracker"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") +BLEClientBase = esp32_ble_client_ns.class_( + "BLEClientBase", esp32_ble_tracker.ESPBTClient, cg.Component +) diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp new file mode 100644 index 0000000000..873833368c --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -0,0 +1,83 @@ +#include "ble_characteristic.h" +#include "ble_client_base.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client.characteristic"; + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLECharacteristic::parse_descriptors() { + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_descr(this->service->client->get_gattc_if(), this->service->client->get_conn_id(), + this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + for (auto &desc : this->descriptors) { + if (desc->uuid == uuid) + return desc; + } + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} +BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) { + for (auto &desc : this->descriptors) { + if (desc->handle == handle) + return desc; + } + return nullptr; +} + +void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { + auto *client = this->service->client; + auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size, + new_val, write_type, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status); + } +} + +void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h new file mode 100644 index 0000000000..ffa9227cc4 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_characteristic.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_descriptor.h" + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEService; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + BLEDescriptor *get_descriptor_by_handle(uint16_t handle); + void write_value(uint8_t *new_val, int16_t new_val_size); + void write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); + BLEService *service; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp new file mode 100644 index 0000000000..0e81c9aca8 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -0,0 +1,324 @@ +#include "ble_client_base.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; + +void BLEClientBase::setup() { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_state(espbt::ClientState::IDLE); +} + +void BLEClientBase::loop() { + if (this->state_ == espbt::ClientState::DISCOVERED) { + this->connect(); + } +} + +float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + +bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) + return false; + if (this->state_ != espbt::ClientState::IDLE) + return false; + + ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); + this->set_state(espbt::ClientState::DISCOVERED); + + auto addr = device.address_uint64(); + this->remote_bda_[0] = (addr >> 40) & 0xFF; + this->remote_bda_[1] = (addr >> 32) & 0xFF; + this->remote_bda_[2] = (addr >> 24) & 0xFF; + this->remote_bda_[3] = (addr >> 16) & 0xFF; + this->remote_bda_[4] = (addr >> 8) & 0xFF; + this->remote_bda_[5] = (addr >> 0) & 0xFF; + this->remote_addr_type_ = device.get_address_type(); + return true; +} + +std::string BLEClientBase::address_str() const { + return str_snprintf("%02x:%02x:%02x:%02x:%02x:%02x", 17, (uint8_t)(this->address_ >> 40) & 0xff, + (uint8_t)(this->address_ >> 32) & 0xff, (uint8_t)(this->address_ >> 24) & 0xff, + (uint8_t)(this->address_ >> 16) & 0xff, (uint8_t)(this->address_ >> 8) & 0xff, + (uint8_t)(this->address_ >> 0) & 0xff); +} + +void BLEClientBase::connect() { + ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); + auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::CONNECTING); + } +} + +void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) + return; + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "gattc registered app id %d", this->app_id); + this->gattc_if_ = esp_gattc_if; + } else { + ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + this->conn_id_ = param->open.conn_id; + if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { + ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + break; + } + case ESP_GATTC_CONNECT_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str()); + if (this->conn_id_ != param->connect.conn_id) { + ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d", + this->address_str().c_str(), param->connect.conn_id, this->conn_id_); + } + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret); + } + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu, + param->cfg_mtu.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); + this->mtu_ = param->cfg_mtu.mtu; + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) { + return; + } + ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason); + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); + this->set_state(espbt::ClientState::IDLE); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) { + ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); + ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); + svc->parse_characteristics(); + } + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + auto *descr = this->get_config_descriptor(param->reg_for_notify.handle); + if (descr == nullptr) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); + break; + } + if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, + descr->uuid.to_string().c_str()); + break; + } + uint16_t notify_en = 1; + auto status = + esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, descr->handle, sizeof(notify_en), + (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + } + break; + } + + default: + break; + } +} + +void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // This event is sent by the server when it requests security + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + // This event is sent once authentication has completed + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_bd_addr_t bd_addr; + memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); + ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str()); + if (!param->ble_security.auth_cmpl.success) { + ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason); + } else { + ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type, + param->ble_security.auth_cmpl.auth_mode); + } + break; + // There are other events we'll want to implement at some point to support things like pass key + // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md + default: + break; + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + if (length > 2) { + return (float) encode_uint16(value[1], value[2]); + } + case 0x7: // uint24. + if (length > 3) { + return (float) encode_uint24(value[1], value[2], value[3]); + } + case 0x8: // uint32. + if (length > 4) { + return (float) encode_uint32(value[1], value[2], value[3], value[4]); + } + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + if (length > 2) { + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + } + case 0xF: // int24. + if (length > 3) { + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + } + case 0x10: // int32. + if (length > 4) { + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + } + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length); + return NAN; +} + +BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { + for (auto *svc : this->services_) { + if (svc->uuid == uuid) + return svc; + } + return nullptr; +} + +BLEService *BLEClientBase::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClientBase::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t handle) { + for (auto *svc : this->services_) { + for (auto *chr : svc->characteristics) { + if (chr->handle == handle) + return chr; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_config_descriptor(uint16_t handle) { + auto *chr = this->get_characteristic(handle); + if (chr != nullptr) { + for (auto &desc : chr->descriptors) { + if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) + return desc; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto *ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) { + for (auto *svc : this->services_) { + for (auto *chr : svc->characteristics) { + for (auto *desc : chr->descriptors) { + if (desc->handle == handle) + return desc; + } + } + } + return nullptr; +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h new file mode 100644 index 0000000000..eba70fa571 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -0,0 +1,72 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" + +#include "ble_service.h" + +#include +#include + +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void connect() override; + + void set_address(uint64_t address) { this->address_ = address; } + std::string address_str() const; + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLECharacteristic *get_characteristic(uint16_t handle); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + BLEDescriptor *get_descriptor(uint16_t handle); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int get_gattc_if() const { return this->gattc_if_; } + uint8_t *get_remote_bda() { return this->remote_bda_; } + esp_ble_addr_type_t get_remote_addr_type() const { return this->remote_addr_type_; } + uint16_t get_conn_id() const { return this->conn_id_; } + uint64_t get_address() const { return this->address_; } + + protected: + int gattc_if_; + esp_bd_addr_t remote_bda_; + esp_ble_addr_type_t remote_addr_type_; + uint16_t conn_id_; + uint64_t address_; + uint16_t mtu_{23}; + + std::vector services_; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_descriptor.h b/esphome/components/esp32_ble_client/ble_descriptor.h new file mode 100644 index 0000000000..c05430144f --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_descriptor.h @@ -0,0 +1,25 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLECharacteristic; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp new file mode 100644 index 0000000000..1d81eaa556 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -0,0 +1,66 @@ +#include "ble_service.h" +#include "ble_client_base.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client.service"; + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + for (auto &chr : this->characteristics) { + if (chr->uuid == uuid) + return chr; + } + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLEService::parse_characteristics() { + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_char(this->client->get_gattc_if(), this->client->get_conn_id(), this->start_handle, + this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory) + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), + characteristic->handle, characteristic->properties); + characteristic->parse_descriptors(); + offset++; + } +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.h b/esphome/components/esp32_ble_client/ble_service.h new file mode 100644 index 0000000000..04f2212e0e --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_service.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_characteristic.h" + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase; + +class BLEService { + public: + ~BLEService(); + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClientBase *client; + void parse_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 6d7868d4c5..0d7abe32f9 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.const import ( + CONF_ACTIVE, CONF_ID, CONF_INTERVAL, CONF_DURATION, @@ -22,7 +23,6 @@ DEPENDENCIES = ["esp32"] CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" -CONF_ACTIVE = "active" CONF_CONTINUOUS = "continuous" CONF_ON_SCAN_END = "on_scan_end" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index f7e51a8ab3..d4512b52b0 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -518,28 +518,39 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } std::string ESPBTUUID::to_string() const { - char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - break; + return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); case ESP_UUID_LEN_32: - sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), - (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); - break; + return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), + this->uuid_.uuid.uuid32 & 0xff); default: case ESP_UUID_LEN_128: - char *bpos = sbuf; + std::string buf; for (int8_t i = 15; i >= 0; i--) { - sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); - bpos += 2; + buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); if (i == 6 || i == 8 || i == 10 || i == 12) - sprintf(bpos++, "-"); + buf += "-"; } - sbuf[47] = '\0'; - break; + return buf; } - return sbuf; + return ""; +} + +uint64_t ESPBTUUID::get_128bit_high() const { + esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); + return ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); +} +uint64_t ESPBTUUID::get_128bit_low() const { + esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); + return ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); } ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 29d0c81542..45abcd63fa 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -41,6 +41,9 @@ class ESPBTUUID { std::string to_string() const; + uint64_t get_128bit_high() const; + uint64_t get_128bit_low() const; + protected: esp_bt_uuid_t uuid_; }; @@ -158,7 +161,7 @@ class ESPBTClient : public ESPBTDeviceListener { esp_ble_gattc_cb_param_t *param) = 0; virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; - void set_state(ClientState st) { this->state_ = st; } + virtual void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } int app_id; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h index f09b2ca8d7..f3a2b3cb3c 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -72,13 +72,13 @@ class BLEEvent { // Need to also make a copy of relevant event data. switch (e) { case ESP_GATTC_NOTIFY_EVT: - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; + this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->data.data(); break; case ESP_GATTC_READ_CHAR_EVT: case ESP_GATTC_READ_DESCR_EVT: - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; + this->data.assign(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param.read.value = this->data.data(); break; default: break; @@ -96,9 +96,9 @@ class BLEEvent { esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; esp_ble_gattc_cb_param_t gattc_param; - uint8_t data[64]; } gattc; } event_; + std::vector data{}; uint8_t type_; // 0=gap 1=gattc }; diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 21638ef7e4..384537e5d7 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -102,8 +102,9 @@ void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); return; } - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, size, blk, - ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size, + blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); } else { diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp index 6bb17f0508..3959178b94 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -50,7 +50,7 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -146,17 +146,17 @@ void RadonEyeRD200::update() { void RadonEyeRD200::write_query_message_() { ESP_LOGV(TAG, "writing 0x50 to write service"); int request = 0x50; - auto status = esp_ble_gattc_write_char_descr(this->parent()->gattc_if, this->parent()->conn_id, this->write_handle_, - sizeof(request), (uint8_t *) &request, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), + this->write_handle_, sizeof(request), (uint8_t *) &request, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status); } } void RadonEyeRD200::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->read_handle_, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), + this->read_handle_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); } diff --git a/esphome/const.py b/esphome/const.py index a272f518df..e9a6975ca2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -23,6 +23,7 @@ CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" From 6b52f62531519451d934fe8cfbe74bbcf0b8564e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:22:01 +1300 Subject: [PATCH 029/106] Bump version to 2022.9.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e9a6975ca2..ff59f2b8bf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.2" +__version__ = "2022.9.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 5df0e82c372362a310b332720e63e47e8284ab01 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 14:42:28 +1300 Subject: [PATCH 030/106] Add network type to mdns service message (#3880) --- esphome/components/mdns/mdns_component.cpp | 6 ++++++ esphome/dashboard/dashboard.py | 1 + esphome/zeroconf.py | 6 +++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 31858c0d3c..e1fcf320ed 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -45,6 +45,12 @@ void MDNSComponent::compile_records_() { service.txt_records.push_back({"board", ESPHOME_BOARD}); +#if defined(USE_WIFI) + service.txt_records.push_back({"network", "wifi"}); +#elif defined(USE_ETHERNET) + service.txt_records.push_back({"network", "ethernet"}); +#endif + #ifdef ESPHOME_PROJECT_NAME service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 9a8f072237..c812ec4b49 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -613,6 +613,7 @@ class ListDevicesHandler(BaseHandler): "package_import_url": res.package_import_url, "project_name": res.project_name, "project_version": res.project_version, + "network": res.network, } for res in IMPORT_RESULT.values() if res.device_name not in configured diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 1fbdf7e93f..089ae4f1c2 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -118,6 +118,7 @@ ESPHOME_SERVICE_TYPE = "_esphomelib._tcp.local." TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url" TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_VERSION = b"project_version" +TXT_RECORD_NETWORK = b"network" @dataclass @@ -126,6 +127,7 @@ class DiscoveredImport: package_import_url: str project_name: str project_version: str + network: str class DashboardImportDiscovery: @@ -134,7 +136,7 @@ class DashboardImportDiscovery: self.service_browser = ServiceBrowser( self.zc, ESPHOME_SERVICE_TYPE, [self._on_update] ) - self.import_state = {} + self.import_state: dict[str, DiscoveredImport] = {} def _on_update( self, @@ -171,12 +173,14 @@ class DashboardImportDiscovery: import_url = info.properties[TXT_RECORD_PACKAGE_IMPORT_URL].decode() project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() + network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode() self.import_state[name] = DiscoveredImport( device_name=node_name, package_import_url=import_url, project_name=project_name, project_version=project_version, + network=network, ) def cancel(self) -> None: From 5e96b8ef7c903d5c4a15a1e66384e0c7ff8dfa0f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 15:20:13 +1300 Subject: [PATCH 031/106] Bump esphome-dashboard to 20221007.0 (#3881) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f86acdbef..00e6e0ba67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.0.2 # When updating platformio, also update Dockerfile esptool==3.3.1 click==8.1.3 -esphome-dashboard==20220925.0 +esphome-dashboard==20221007.0 aioesphomeapi==10.13.0 zeroconf==0.39.1 From 3b83f967e4859ae800ff9448cc47034ef045a011 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 15:35:48 +1300 Subject: [PATCH 032/106] Dont add wifi block to yaml if discovered device uses ethernet (#3882) --- esphome/components/dashboard_import/__init__.py | 15 ++++++++++----- esphome/dashboard/dashboard.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 41b4a8bed1..b795c85b12 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path import esphome.codegen as cg import esphome.config_validation as cv from esphome.components.packages import validate_source_shorthand +from esphome.const import CONF_WIFI from esphome.wizard import wizard_file from esphome.yaml_util import dump @@ -43,7 +44,9 @@ async def to_code(config): cg.add(dashboard_import_ns.set_package_import_url(config[CONF_PACKAGE_IMPORT_URL])) -def import_config(path: str, name: str, project_name: str, import_url: str) -> None: +def import_config( + path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI +) -> None: p = Path(path) if p.exists(): @@ -69,7 +72,9 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N "name_add_mac_suffix": False, }, } - p.write_text( - dump(config) + WIFI_CONFIG, - encoding="utf8", - ) + output = dump(config) + + if network == CONF_WIFI: + output += WIFI_CONFIG + + p.write_text(output, encoding="utf8") diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c812ec4b49..07ee151949 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -395,11 +395,22 @@ class ImportRequestHandler(BaseHandler): args = json.loads(self.request.body.decode()) try: name = args["name"] + + imported_device = next( + (res for res in IMPORT_RESULT.values() if res.device_name == name), None + ) + + if imported_device is not None: + network = imported_device.network + else: + network = const.CONF_WIFI + import_config( settings.rel_path(f"{name}.yaml"), name, args["project_name"], args["package_import_url"], + network, ) except FileExistsError: self.set_status(500) From 3c320c4c836c4611ac8171b0b69be3cdbfc40ee0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:34:39 +1300 Subject: [PATCH 033/106] Bump version to 2022.9.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ff59f2b8bf..e93808e6d4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.3" +__version__ = "2022.9.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From bf15b1d302a05a9d73bdd5c856472b51d91b6e72 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Oct 2022 09:18:46 +1300 Subject: [PATCH 034/106] Bump version to 2022.11.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7dcb3c823c..f2d31bd21f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.10.0-dev" +__version__ = "2022.11.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 98171c9f499868314b0a862512c03d967a7bcb5c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 12 Oct 2022 18:11:59 -0300 Subject: [PATCH 035/106] fix never calling preset change trigger (#3864) Co-authored-by: Keith Burzinski --- .../thermostat/thermostat_climate.cpp | 73 +++++++++++++------ .../thermostat/thermostat_climate.h | 3 +- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 54e9f1687c..61e279d4a6 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -974,9 +974,19 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || + this->preset.value() != preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } else { + ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } this->custom_preset.reset(); this->preset = preset; } else { @@ -988,9 +998,19 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str()); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || + this->custom_preset.value() != custom_preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + } else { + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + } this->preset.reset(); this->custom_preset = custom_preset; } else { @@ -998,39 +1018,46 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) } } -void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { +bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { + bool something_changed = false; + if (this->supports_two_points_) { - this->target_temperature_low = config.default_temperature_low; - this->target_temperature_high = config.default_temperature_high; + if (this->target_temperature_low != config.default_temperature_low) { + this->target_temperature_low = config.default_temperature_low; + something_changed = true; + } + if (this->target_temperature_high != config.default_temperature_high) { + this->target_temperature_high = config.default_temperature_high; + something_changed = true; + } } else { - this->target_temperature = config.default_temperature; + if (this->target_temperature != config.default_temperature) { + this->target_temperature = config.default_temperature; + something_changed = true; + } } - // Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call - // also specifies them then the control's version will override these for that call - if (config.mode_.has_value()) { - this->mode = *config.mode_; + // Note: The mode, fan_mode and swing_mode can all be defined in the preset but if the climate.control call + // also specifies them then the climate.control call's values will override the preset's values for that call + if (config.mode_.has_value() && (this->mode != config.mode_.value())) { ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + this->mode = *config.mode_; + something_changed = true; } - if (config.fan_mode_.has_value()) { - this->fan_mode = *config.fan_mode_; + if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + this->fan_mode = *config.fan_mode_; + something_changed = true; } - if (config.swing_mode_.has_value()) { + if (config.swing_mode_.has_value() && (this->swing_mode != config.swing_mode_.value())) { ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); this->swing_mode = *config.swing_mode_; + something_changed = true; } - // Fire any preset changed trigger if defined - if (this->preset != preset) { - Trigger<> *trig = this->preset_change_trigger_; - assert(trig != nullptr); - trig->trigger(); - } - - this->refresh(); + return something_changed; } void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index a738ba4986..68cbb17e34 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -168,7 +168,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset - void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); + /// Returns true if something was changed + bool change_preset_internal_(const ThermostatClimateTargetTempConfig &config); /// Return the traits of this controller. climate::ClimateTraits traits() override; From 71387be72e36804773748d20c623d3e9cc7963d7 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Thu, 13 Oct 2022 03:50:45 +0400 Subject: [PATCH 036/106] Modbus QWORD fix (#3856) --- .../modbus_controller/modbus_controller.cpp | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index e60b016a17..57f714f233 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -571,24 +571,16 @@ int64_t payload_to_number(const std::vector &data, SensorValueType sens static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); } break; case SensorValueType::U_QWORD: - // Ignore bitmask for U_QWORD - value = get_data(data, offset); - break; case SensorValueType::S_QWORD: - // Ignore bitmask for S_QWORD - value = get_data(data, offset); + // Ignore bitmask for QWORD + value = get_data(data, offset); break; case SensorValueType::U_QWORD_R: - // Ignore bitmask for U_QWORD - value = get_data(data, offset); - value = static_cast(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | - static_cast(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | - static_cast(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; - break; - case SensorValueType::S_QWORD_R: - // Ignore bitmask for S_QWORD - value = get_data(data, offset); - break; + case SensorValueType::S_QWORD_R: { + // Ignore bitmask for QWORD + uint64_t tmp = get_data(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); + } break; case SensorValueType::RAW: default: break; From 5ec158811025db6932bbbd999ce6cf322cd89287 Mon Sep 17 00:00:00 2001 From: Frank Riley Date: Wed, 12 Oct 2022 16:59:07 -0700 Subject: [PATCH 037/106] Update the ibeacon code (#3859) --- .../components/esp32_ble_beacon/__init__.py | 57 +++++++++++++++---- .../esp32_ble_beacon/esp32_ble_beacon.cpp | 30 ++++++++-- .../esp32_ble_beacon/esp32_ble_beacon.h | 13 ++++- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index d6cbb15dd2..c3bd4ee418 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID -from esphome.core import CORE +from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER +from esphome.core import CORE, TimePeriod from esphome.components.esp32 import add_idf_sdkconfig_option DEPENDENCIES = ["esp32"] @@ -12,16 +12,47 @@ ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) CONF_MAJOR = "major" CONF_MINOR = "minor" +CONF_MIN_INTERVAL = "min_interval" +CONF_MAX_INTERVAL = "max_interval" +CONF_MEASURED_POWER = "measured_power" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), - cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), - cv.Required(CONF_UUID): cv.uuid, - cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, - cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, - } -).extend(cv.COMPONENT_SCHEMA) + +def validate_config(config): + if config[CONF_MIN_INTERVAL] > config.get(CONF_MAX_INTERVAL): + raise cv.Invalid("min_interval must be <= max_interval") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), + cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), + cv.Required(CONF_UUID): cv.uuid, + cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, + cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, + cv.Optional(CONF_MIN_INTERVAL, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240) + ), + ), + cv.Optional(CONF_MAX_INTERVAL, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240) + ), + ), + cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( + min=-128, max=0 + ), + cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( + cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True) + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_config, +) async def to_code(config): @@ -31,6 +62,10 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_major(config[CONF_MAJOR])) cg.add(var.set_minor(config[CONF_MINOR])) + cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) + cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) + cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) + cg.add(var.set_tx_power(config[CONF_TX_POWER])) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 955bc8595f..589fcc1e82 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,24 @@ static esp_ble_adv_params_t ble_adv_params = { #define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8)) static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { - .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; + .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}}; void ESP32BLEBeacon::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:"); - ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_); + char uuid[37]; + char *bpos = uuid; + for (int8_t ii = 0; ii < 16; ++ii) { + bpos += sprintf(bpos, "%02X", this->uuid_[ii]); + if (ii == 3 || ii == 5 || ii == 7 || ii == 9) { + bpos += sprintf(bpos, "-"); + } + } + uuid[36] = '\0'; + ESP_LOGCONFIG(TAG, + " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" + ", TX Power: %ddBm", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, + this->tx_power_); } void ESP32BLEBeacon::setup() { @@ -67,6 +80,9 @@ void ESP32BLEBeacon::ble_core_task(void *params) { } void ESP32BLEBeacon::ble_setup() { + ble_adv_params.adv_int_min = static_cast(global_esp32_ble_beacon->min_interval_ / 0.625f); + ble_adv_params.adv_int_max = static_cast(global_esp32_ble_beacon->max_interval_ / 0.625f); + // Initialize non-volatile storage for the bluetooth controller esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { @@ -118,6 +134,12 @@ void ESP32BLEBeacon::ble_setup() { ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); return; } + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, + static_cast((global_esp32_ble_beacon->tx_power_ + 12) / 3)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); + return; + } err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); @@ -130,7 +152,7 @@ void ESP32BLEBeacon::ble_setup() { sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_); - ibeacon_adv_data.ibeacon_vendor.measured_power = 0xC5; + ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(global_esp32_ble_beacon->measured_power_); esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); } @@ -153,7 +175,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap break; } case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: { - err = param->adv_start_cmpl.status; + err = param->adv_stop_cmpl.status; if (err != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG, "BLE adv stop failed: %s", esp_err_to_name(err)); } else { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 80ad2041f2..5208b67ea3 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -5,6 +5,7 @@ #ifdef USE_ESP32 #include +#include namespace esphome { namespace esp32_ble_beacon { @@ -14,8 +15,8 @@ typedef struct { uint8_t flags[3]; uint8_t length; uint8_t type; - uint16_t company_id; - uint16_t beacon_type; + uint8_t company_id[2]; + uint8_t beacon_type[2]; } __attribute__((packed)) esp_ble_ibeacon_head_t; // NOLINTNEXTLINE(modernize-use-using) @@ -42,6 +43,10 @@ class ESP32BLEBeacon : public Component { void set_major(uint16_t major) { this->major_ = major; } void set_minor(uint16_t minor) { this->minor_ = minor; } + void set_min_interval(uint16_t val) { this->min_interval_ = val; } + void set_max_interval(uint16_t val) { this->max_interval_ = val; } + void set_measured_power(int8_t val) { this->measured_power_ = val; } + void set_tx_power(int8_t val) { this->tx_power_ = val; } protected: static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); @@ -51,6 +56,10 @@ class ESP32BLEBeacon : public Component { std::array uuid_; uint16_t major_{}; uint16_t minor_{}; + uint16_t min_interval_{}; + uint16_t max_interval_{}; + int8_t measured_power_{}; + int8_t tx_power_{}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) From 3b21d1d81eef1a5fac74b3f9239376cf180ae208 Mon Sep 17 00:00:00 2001 From: Brian Kaufman Date: Thu, 13 Oct 2022 12:55:59 -0700 Subject: [PATCH 038/106] Don't Use Base Network Manual IP for WiFi AP (#3902) --- esphome/components/wifi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index a4c246da89..93ea6b92a4 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -368,7 +368,7 @@ async def to_code(config): if CONF_AP in config: conf = config[CONF_AP] - ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) + ip_config = conf.get(CONF_MANUAL_IP) cg.with_local_variable( conf[CONF_ID], WiFiAP(), From 4bf94e0757420d97d7bd4c1d1950d8efea1477b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 13 Oct 2022 21:58:42 +0200 Subject: [PATCH 039/106] Allow preserving WiFi credentials entered with captive_portal (#3813) --- esphome/components/captive_portal/__init__.py | 24 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index f024c94b01..b41095304e 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_NETWORKS, CONF_PASSWORD, CONF_SSID, CONF_WIFI from esphome.core import coroutine_with_priority, CORE AUTO_LOAD = ["web_server_base"] @@ -12,6 +13,7 @@ CODEOWNERS = ["@OttoWinter"] captive_portal_ns = cg.esphome_ns.namespace("captive_portal") CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) +CONF_KEEP_USER_CREDENTIALS = "keep_user_credentials" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -19,12 +21,29 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), + cv.Optional(CONF_KEEP_USER_CREDENTIALS, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_arduino, ) +def validate_wifi(config): + wifi_conf = fv.full_config.get()[CONF_WIFI] + if config.get(CONF_KEEP_USER_CREDENTIALS, False) and ( + CONF_SSID in wifi_conf + or CONF_PASSWORD in wifi_conf + or CONF_NETWORKS in wifi_conf + ): + raise cv.Invalid( + f"WiFi credentials cannot be used together with {CONF_KEEP_USER_CREDENTIALS}" + ) + return config + + +FINAL_VALIDATE_SCHEMA = validate_wifi + + @coroutine_with_priority(64.0) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) @@ -38,3 +57,6 @@ async def to_code(config): cg.add_library("WiFi", None) if CORE.is_esp8266: cg.add_library("DNSServer", None) + + if config.get(CONF_KEEP_USER_CREDENTIALS, False): + cg.add_define("USE_CAPTIVE_PORTAL_KEEP_USER_CREDENTIALS") diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 37f5276c8f..0103106222 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -38,7 +38,11 @@ void WiFiComponent::setup() { this->last_connected_ = millis(); this->wifi_pre_setup_(); +#ifndef USE_CAPTIVE_PORTAL_KEEP_USER_CREDENTIALS uint32_t hash = fnv1_hash(App.get_compilation_time()); +#else + uint32_t hash = 88491487UL; +#endif this->pref_ = global_preferences->make_preference(hash, true); SavedWifiSettings save{}; From 225b3c14943f698de57e1ad17fee97c56e13b36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Jouault?= Date: Fri, 14 Oct 2022 01:47:05 +0200 Subject: [PATCH 040/106] Send true and not RSSI in ble_presence (#3904) --- esphome/components/ble_presence/ble_presence_device.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index dcccf844d2..1689c9ba3f 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -58,7 +58,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { - this->publish_state(device.get_rssi()); + this->publish_state(true); this->found_ = true; return true; } @@ -83,7 +83,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, return false; } - this->publish_state(device.get_rssi()); + this->publish_state(true); this->found_ = true; return true; } From 8bb670521d3d51632a64781753219a9df9bc9f71 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 15 Oct 2022 08:35:35 +1300 Subject: [PATCH 041/106] Remove address type map from bluetooth proxy (#3905) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 21 ------------------- .../bluetooth_proxy/bluetooth_proxy.h | 1 - 2 files changed, 22 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a08c58bd9e..cc1c178b08 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -26,16 +26,9 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) device.get_rssi()); this->send_api_packet_(device); - this->address_type_map_[device.address_uint64()] = device.get_address_type(); - if (this->address_ == 0) return true; - if (this->state_ == espbt::ClientState::DISCOVERED) { - ESP_LOGV(TAG, "Connecting to address %s", this->address_str().c_str()); - return true; - } - BLEClientBase::parse_device(device); return true; } @@ -216,20 +209,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest switch (msg.request_type) { case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { this->address_ = msg.address; - if (this->address_type_map_.find(this->address_) != this->address_type_map_.end()) { - // Utilise the address type cache - this->remote_addr_type_ = this->address_type_map_[this->address_]; - } else { - this->remote_addr_type_ = BLE_ADDR_TYPE_PUBLIC; - } - this->remote_bda_[0] = (this->address_ >> 40) & 0xFF; - this->remote_bda_[1] = (this->address_ >> 32) & 0xFF; - this->remote_bda_[2] = (this->address_ >> 24) & 0xFF; - this->remote_bda_[3] = (this->address_ >> 16) & 0xFF; - this->remote_bda_[4] = (this->address_ >> 8) & 0xFF; - this->remote_bda_[5] = (this->address_ >> 0) & 0xFF; - this->set_state(espbt::ClientState::DISCOVERED); - esp_ble_gap_stop_scanning(); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 97047c118b..9529b99f73 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -45,7 +45,6 @@ class BluetoothProxy : public BLEClientBase { protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); - std::map address_type_map_; int16_t send_service_{-1}; bool active_; }; From d1263e583b2b5c41fe73fa3668cae4a5b960a088 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Oct 2022 10:30:22 +1300 Subject: [PATCH 042/106] Bump version to 2022.10.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9d608ebf9b..d2bac8f9df 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.10.0b2" +__version__ = "2022.10.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 58cd754e0733c22b456e2472e730e9d84b6f12f4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Oct 2022 10:50:32 +1300 Subject: [PATCH 043/106] Fix bad merge --- .../bluetooth_proxy/bluetooth_proxy.cpp | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 3f06cd5c25..cc1c178b08 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -164,78 +164,6 @@ void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if } } -void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - BLEClientBase::gattc_event_handler(event, gattc_if, param); - switch (event) { - case ESP_GATTC_DISCONNECT_EVT: { -#ifdef USE_API - api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, - param->disconnect.reason); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); -#endif - this->address_ = 0; - } - case ESP_GATTC_OPEN_EVT: { - if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { -#ifdef USE_API - api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, param->open.status); - -#endif - break; - } - break; - } - case ESP_GATTC_SEARCH_CMPL_EVT: { -#ifdef USE_API - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); -#endif - break; - } - case ESP_GATTC_READ_DESCR_EVT: - case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->conn_id_) - break; - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error reading char/descriptor at handle %d, status=%d", param->read.handle, param->read.status); - break; - } -#ifdef USE_API - api::BluetoothGATTReadResponse resp; - resp.address = this->address_; - resp.handle = param->read.handle; - resp.data.reserve(param->read.value_len); - for (uint16_t i = 0; i < param->read.value_len; i++) { - resp.data.push_back(param->read.value[i]); - } - api::global_api_server->send_bluetooth_gatt_read_response(resp); -#endif - break; - } - case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->conn_id_) - break; - ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT: handle=0x%x", param->notify.handle); -#ifdef USE_API - api::BluetoothGATTNotifyDataResponse resp; - resp.address = this->address_; - resp.handle = param->notify.handle; - resp.data.reserve(param->notify.value_len); - for (uint16_t i = 0; i < param->notify.value_len; i++) { - resp.data.push_back(param->notify.value[i]); - } - api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); -#endif - break; - } - default: - break; - } -} - void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); } void BluetoothProxy::loop() { From 4ac72d7d08bc54f06751424ef57b1e794526745b Mon Sep 17 00:00:00 2001 From: Marcel Hoppe Date: Wed, 19 Oct 2022 02:44:26 +0200 Subject: [PATCH 044/106] Add support for wl-134 (#3569) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/wl_134/__init__.py | 0 esphome/components/wl_134/text_sensor.py | 31 +++++++ esphome/components/wl_134/wl_134.cpp | 111 +++++++++++++++++++++++ esphome/components/wl_134/wl_134.h | 63 +++++++++++++ 5 files changed, 206 insertions(+) create mode 100644 esphome/components/wl_134/__init__.py create mode 100644 esphome/components/wl_134/text_sensor.py create mode 100644 esphome/components/wl_134/wl_134.cpp create mode 100644 esphome/components/wl_134/wl_134.h diff --git a/CODEOWNERS b/CODEOWNERS index b04b480780..4ac394345c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -254,6 +254,7 @@ esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze +esphome/components/wl_134/* @hobbypunk90 esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/esphome/components/wl_134/__init__.py b/esphome/components/wl_134/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wl_134/text_sensor.py b/esphome/components/wl_134/text_sensor.py new file mode 100644 index 0000000000..1373df77f4 --- /dev/null +++ b/esphome/components/wl_134/text_sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, uart +from esphome.const import ( + ICON_FINGERPRINT, +) + +CODEOWNERS = ["@hobbypunk90"] +DEPENDENCIES = ["uart"] +CONF_RESET = "reset" + +wl134_ns = cg.esphome_ns.namespace("wl_134") +Wl134Component = wl134_ns.class_( + "Wl134Component", text_sensor.TextSensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema( + Wl134Component, + icon=ICON_FINGERPRINT, + ) + .extend({cv.Optional(CONF_RESET, default=False): cv.boolean}) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_do_reset(config[CONF_RESET])) + await uart.register_uart_device(var, config) diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp new file mode 100644 index 0000000000..3ffa0c63ce --- /dev/null +++ b/esphome/components/wl_134/wl_134.cpp @@ -0,0 +1,111 @@ +#include "wl_134.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wl_134 { + +static const char *const TAG = "wl_134.sensor"; +static const uint8_t ASCII_CR = 0x0D; +static const uint8_t ASCII_NBSP = 0xFF; +static const int MAX_DATA_LENGTH_BYTES = 6; + +void Wl134Component::setup() { this->publish_state(""); } + +void Wl134Component::loop() { + while (this->available() >= RFID134_PACKET_SIZE) { + Wl134Component::Rfid134Error error = this->read_packet_(); + if (error != RFID134_ERROR_NONE) { + ESP_LOGW(TAG, "Error: %d", error); + } + } +} + +Wl134Component::Rfid134Error Wl134Component::read_packet_() { + uint8_t packet[RFID134_PACKET_SIZE]; + packet[RFID134_PACKET_START_CODE] = this->read(); + + // check for the first byte being the packet start code + if (packet[RFID134_PACKET_START_CODE] != 0x02) { + // just out of sync, ignore until we are synced up + return RFID134_ERROR_NONE; + } + + if (!this->read_array(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_SIZE - 1)) { + return RFID134_ERROR_PACKET_SIZE; + } + + if (packet[RFID134_PACKET_END_CODE] != 0x03) { + return RFID134_ERROR_PACKET_END_CODE_MISSMATCH; + } + + // calculate checksum + uint8_t checksum = 0; + for (uint8_t i = RFID134_PACKET_ID; i < RFID134_PACKET_CHECKSUM; i++) { + checksum = checksum ^ packet[i]; + } + + // test checksum + if (checksum != packet[RFID134_PACKET_CHECKSUM]) { + return RFID134_ERROR_PACKET_CHECKSUM; + } + + if (static_cast(~checksum) != static_cast(packet[RFID134_PACKET_CHECKSUM_INVERT])) { + return RFID134_ERROR_PACKET_CHECKSUM_INVERT; + } + + Rfid134Reading reading; + + // convert packet into the reading struct + reading.id = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_COUNTRY - RFID134_PACKET_ID); + reading.country = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_COUNTRY]), + RFID134_PACKET_DATA_FLAG - RFID134_PACKET_COUNTRY); + reading.isData = packet[RFID134_PACKET_DATA_FLAG] == '1'; + reading.isAnimal = packet[RFID134_PACKET_ANIMAL_FLAG] == '1'; + reading.reserved0 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED0]), + RFID134_PACKET_RESERVED1 - RFID134_PACKET_RESERVED0); + reading.reserved1 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED1]), + RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); + + ESP_LOGV(TAG, "Tag id: %012lld", reading.id); + ESP_LOGV(TAG, "Country: %03d", reading.country); + ESP_LOGV(TAG, "isData: %s", reading.isData ? "true" : "false"); + ESP_LOGV(TAG, "isAnimal: %s", reading.isAnimal ? "true" : "false"); + ESP_LOGV(TAG, "Reserved0: %d", reading.reserved0); + ESP_LOGV(TAG, "Reserved1: %d", reading.reserved1); + + char buf[20]; + sprintf(buf, "%03d%012lld", reading.country, reading.id); + this->publish_state(buf); + if (this->do_reset_) { + this->set_timeout(1000, [this]() { this->publish_state(""); }); + } + + return RFID134_ERROR_NONE; +} + +uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size) { + uint64_t value = 0; + uint8_t i = text_size; + do { + i--; + + uint8_t digit = text[i]; + if (digit >= 'A') { + digit = digit - 'A' + 10; + } else { + digit = digit - '0'; + } + value = (value << 4) + digit; + } while (i != 0); + + return value; +} + +void Wl134Component::dump_config() { + ESP_LOGCONFIG(TAG, "WL-134 Sensor:"); + LOG_TEXT_SENSOR("", "Tag", this); + // As specified in the sensor's data sheet + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); +} +} // namespace wl_134 +} // namespace esphome diff --git a/esphome/components/wl_134/wl_134.h b/esphome/components/wl_134/wl_134.h new file mode 100644 index 0000000000..c0a90de17d --- /dev/null +++ b/esphome/components/wl_134/wl_134.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace wl_134 { + +class Wl134Component : public text_sensor::TextSensor, public Component, public uart::UARTDevice { + public: + enum Rfid134Error { + RFID134_ERROR_NONE, + + // from library + RFID134_ERROR_PACKET_SIZE = 0x81, + RFID134_ERROR_PACKET_END_CODE_MISSMATCH, + RFID134_ERROR_PACKET_CHECKSUM, + RFID134_ERROR_PACKET_CHECKSUM_INVERT + }; + + struct Rfid134Reading { + uint16_t country; + uint64_t id; + bool isData; + bool isAnimal; + uint16_t reserved0; + uint32_t reserved1; + }; + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void setup() override; + void loop() override; + void dump_config() override; + + void set_do_reset(bool do_reset) { this->do_reset_ = do_reset; } + + private: + enum DfMp3Packet { + RFID134_PACKET_START_CODE, + RFID134_PACKET_ID = 1, + RFID134_PACKET_COUNTRY = 11, + RFID134_PACKET_DATA_FLAG = 15, + RFID134_PACKET_ANIMAL_FLAG = 16, + RFID134_PACKET_RESERVED0 = 17, + RFID134_PACKET_RESERVED1 = 21, + RFID134_PACKET_CHECKSUM = 27, + RFID134_PACKET_CHECKSUM_INVERT = 28, + RFID134_PACKET_END_CODE = 29, + RFID134_PACKET_SIZE + }; + + bool do_reset_; + + Rfid134Error read_packet_(); + uint64_t hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size); +}; + +} // namespace wl_134 +} // namespace esphome From 41b5cb06d3d9a58a08fed080741bef35404e149f Mon Sep 17 00:00:00 2001 From: Jadson Santos <42282908+gtjadsonsantos@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:44:48 -0300 Subject: [PATCH 045/106] New platform ethernet_info from component text_sensor (#3811) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ethernet_info/__init__.py | 1 + .../ethernet_info_text_sensor.cpp | 16 +++++++++ .../ethernet_info/ethernet_info_text_sensor.h | 35 +++++++++++++++++++ .../components/ethernet_info/text_sensor.py | 34 ++++++++++++++++++ esphome/core/component.cpp | 1 + esphome/core/component.h | 1 + tests/test4.yaml | 3 ++ 8 files changed, 92 insertions(+) create mode 100644 esphome/components/ethernet_info/__init__.py create mode 100644 esphome/components/ethernet_info/ethernet_info_text_sensor.cpp create mode 100644 esphome/components/ethernet_info/ethernet_info_text_sensor.h create mode 100644 esphome/components/ethernet_info/text_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4ac394345c..d3151c3f02 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -76,6 +76,7 @@ esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_improv/* @jesserockz esphome/components/esp8266/* @esphome/core +esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/factory_reset/* @anatoly-savchenkov diff --git a/esphome/components/ethernet_info/__init__.py b/esphome/components/ethernet_info/__init__.py new file mode 100644 index 0000000000..312688d3fd --- /dev/null +++ b/esphome/components/ethernet_info/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gtjadsonsantos"] diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp new file mode 100644 index 0000000000..e69872c290 --- /dev/null +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -0,0 +1,16 @@ +#include "ethernet_info_text_sensor.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +namespace esphome { +namespace ethernet_info { + +static const char *const TAG = "ethernet_info"; + +void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); } + +} // namespace ethernet_info +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h new file mode 100644 index 0000000000..c683b68080 --- /dev/null +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/ethernet/ethernet_component.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +namespace esphome { +namespace ethernet_info { + +class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { + public: + void update() override { + tcpip_adapter_ip_info_t tcpip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &tcpip); + auto ip = tcpip.ip.addr; + if (ip != this->last_ip_) { + this->last_ip_ = ip; + this->publish_state(network::IPAddress(ip).str()); + } + } + + float get_setup_priority() const override { return setup_priority::ETHERNET; } + std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } + void dump_config() override; + + protected: + network::IPAddress last_ip_; +}; + +} // namespace ethernet_info +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py new file mode 100644 index 0000000000..7cb9944c92 --- /dev/null +++ b/esphome/components/ethernet_info/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_IP_ADDRESS, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +DEPENDENCIES = ["ethernet"] + +ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info") + +IPAddressEsthernetInfo = ethernet_info_ns.class_( + "IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( + IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")) + } +) + + +async def setup_conf(config, key): + if key in config: + conf = config[key] + var = await text_sensor.new_text_sensor(conf) + await cg.register_component(var, conf) + + +async def to_code(config): + await setup_conf(config, CONF_IP_ADDRESS) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5cb063cbec..7505e1f39b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float ETHERNET = 250.0f; const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; diff --git a/esphome/core/component.h b/esphome/core/component.h index cb97a93d21..1d8499e262 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -29,6 +29,7 @@ extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +extern const float ETHERNET; /// For components that should be initialized after WiFi and before API is connected. extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. diff --git a/tests/test4.yaml b/tests/test4.yaml index cf517bb391..a8077c625f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -532,6 +532,9 @@ text_sensor: - platform: copy source_id: inverter0_device_mode name: Inverter Text Sensor Copy + - platform: ethernet_info + ip_address: + name: IP Address output: - platform: pipsolar From f30e54d1770f1e38e9a10931fa9199f6dbbe9f9e Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Tue, 18 Oct 2022 22:08:27 -0500 Subject: [PATCH 046/106] Implementation for Atlas Scientific Peristaltic Pump (#3528) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ezo_pmp/__init__.py | 291 +++++++++++ esphome/components/ezo_pmp/binary_sensor.py | 42 ++ esphome/components/ezo_pmp/ezo_pmp.cpp | 542 ++++++++++++++++++++ esphome/components/ezo_pmp/ezo_pmp.h | 252 +++++++++ esphome/components/ezo_pmp/sensor.py | 104 ++++ esphome/components/ezo_pmp/text_sensor.py | 39 ++ tests/test5.yaml | 54 ++ 8 files changed, 1325 insertions(+) create mode 100644 esphome/components/ezo_pmp/__init__.py create mode 100644 esphome/components/ezo_pmp/binary_sensor.py create mode 100644 esphome/components/ezo_pmp/ezo_pmp.cpp create mode 100644 esphome/components/ezo_pmp/ezo_pmp.h create mode 100644 esphome/components/ezo_pmp/sensor.py create mode 100644 esphome/components/ezo_pmp/text_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d3151c3f02..86e6cd978b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -79,6 +79,7 @@ esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb +esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py new file mode 100644 index 0000000000..e65fcf74ca --- /dev/null +++ b/esphome/components/ezo_pmp/__init__.py @@ -0,0 +1,291 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION +from esphome import automation +from esphome.automation import maybe_simple_id + +CODEOWNERS = ["@carlos-sarmiento"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_VOLUME = "volume" +CONF_VOLUME_PER_MINUTE = "volume_per_minute" + +ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp") +EzoPMP = ezo_pmp_ns.class_("EzoPMP", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EzoPMP), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(103)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +EZO_PMP_NO_ARGS_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + } +) + +# Actions that do not require more arguments + +EzoPMPFindAction = ezo_pmp_ns.class_("EzoPMPFindAction", automation.Action) +EzoPMPClearTotalVolumeDispensedAction = ezo_pmp_ns.class_( + "EzoPMPClearTotalVolumeDispensedAction", automation.Action +) +EzoPMPClearCalibrationAction = ezo_pmp_ns.class_( + "EzoPMPClearCalibrationAction", automation.Action +) +EzoPMPPauseDosingAction = ezo_pmp_ns.class_( + "EzoPMPPauseDosingAction", automation.Action +) +EzoPMPStopDosingAction = ezo_pmp_ns.class_("EzoPMPStopDosingAction", automation.Action) +EzoPMPDoseContinuouslyAction = ezo_pmp_ns.class_( + "EzoPMPDoseContinuouslyAction", automation.Action +) + +# Actions that require more arguments +EzoPMPDoseVolumeAction = ezo_pmp_ns.class_("EzoPMPDoseVolumeAction", automation.Action) +EzoPMPDoseVolumeOverTimeAction = ezo_pmp_ns.class_( + "EzoPMPDoseVolumeOverTimeAction", automation.Action +) +EzoPMPDoseWithConstantFlowRateAction = ezo_pmp_ns.class_( + "EzoPMPDoseWithConstantFlowRateAction", automation.Action +) +EzoPMPSetCalibrationVolumeAction = ezo_pmp_ns.class_( + "EzoPMPSetCalibrationVolumeAction", automation.Action +) +EzoPMPChangeI2CAddressAction = ezo_pmp_ns.class_( + "EzoPMPChangeI2CAddressAction", automation.Action +) +EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( + "EzoPMPArbitraryCommandAction", automation.Action +) + + +@automation.register_action( + "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_find_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.dose_continuously", + EzoPMPDoseContinuouslyAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.clear_total_volume_dosed", + EzoPMPClearTotalVolumeDispensedAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_total_volume_dosed_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.clear_calibration", + EzoPMPClearCalibrationAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +# Actions that require Multiple Args + +EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA +) +async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume_over_time", + EzoPMPDoseVolumeOverTimeAction, + EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, +) +async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME_PER_MINUTE): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_with_constant_flow_rate", + EzoPMPDoseWithConstantFlowRateAction, + EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, +) +async def ezo_pmp_dose_with_constant_flow_rate_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME_PER_MINUTE], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.set_calibration_volume", + EzoPMPSetCalibrationVolumeAction, + EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, +) +async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_ADDRESS): cv.templatable(cv.int_range(min=1, max=127)), + } +) + + +@automation.register_action( + "ezo_pmp.change_i2c_address", + EzoPMPChangeI2CAddressAction, + EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, +) +async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double) + cg.add(var.set_address(template_)) + + return var + + +EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_COMMAND): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "ezo_pmp.arbitrary_command", + EzoPMPArbitraryCommandAction, + EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, +) +async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string) + cg.add(var.set_command(template_)) + + return var diff --git a/esphome/components/ezo_pmp/binary_sensor.py b/esphome/components/ezo_pmp/binary_sensor.py new file mode 100644 index 0000000000..582eb7af25 --- /dev/null +++ b/esphome/components/ezo_pmp/binary_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_RUNNING, + DEVICE_CLASS_EMPTY, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_PUMP_STATE = "pump_state" +CONF_IS_PAUSED = "is_paused" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_PUMP_STATE): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_IS_PAUSED): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_EMPTY, + entity_category=ENTITY_CATEGORY_NONE, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_PUMP_STATE in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_PUMP_STATE]) + cg.add(parent.set_is_dosing(sens)) + + if CONF_IS_PAUSED in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_IS_PAUSED]) + cg.add(parent.set_is_paused(sens)) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp new file mode 100644 index 0000000000..9c96ce2af0 --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -0,0 +1,542 @@ +#include "ezo_pmp.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ezo_pmp { + +static const char *const TAG = "ezo-pmp"; + +static const uint16_t EZO_PMP_COMMAND_NONE = 0; +static const uint16_t EZO_PMP_COMMAND_TYPE_READ = 1; + +static const uint16_t EZO_PMP_COMMAND_FIND = 2; +static const uint16_t EZO_PMP_COMMAND_DOSE_CONTINUOUSLY = 4; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME = 8; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME = 16; +static const uint16_t EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE = 32; +static const uint16_t EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME = 64; +static const uint16_t EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED = 128; +static const uint16_t EZO_PMP_COMMAND_CLEAR_CALIBRATION = 256; +static const uint16_t EZO_PMP_COMMAND_PAUSE_DOSING = 512; +static const uint16_t EZO_PMP_COMMAND_STOP_DOSING = 1024; +static const uint16_t EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS = 2048; +static const uint16_t EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS = 4096; + +static const uint16_t EZO_PMP_COMMAND_READ_DOSING = 3; +static const uint16_t EZO_PMP_COMMAND_READ_SINGLE_REPORT = 5; +static const uint16_t EZO_PMP_COMMAND_READ_MAX_FLOW_RATE = 9; +static const uint16_t EZO_PMP_COMMAND_READ_PAUSE_STATUS = 17; +static const uint16_t EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED = 33; +static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65; +static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129; +static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257; + +static const std::string DOSING_MODE_NONE = "None"; +static const std::string DOSING_MODE_VOLUME = "Volume"; +static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time"; +static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate"; +static const std::string DOSING_MODE_CONTINUOUS = "Continuous"; + +void EzoPMP::dump_config() { + LOG_I2C_DEVICE(this); + if (this->is_failed()) + ESP_LOGE(TAG, "Communication with EZO-PMP circuit failed!"); + LOG_UPDATE_INTERVAL(this); +} + +void EzoPMP::update() { + if (this->is_waiting_) { + return; + } + + if (this->is_first_read_) { + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); + this->is_first_read_ = false; + } + + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + + if (this->is_dosing_flag_) { + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + } + + this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_); + } else { + ESP_LOGV(TAG, "Not Scheduling new Command during update()"); + } +} + +void EzoPMP::loop() { + // If we are not waiting for anything and there is no command to be sent, return + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + return; + } + + // If we are not waiting for anything and there IS a command to be sent, do it. + if (!this->is_waiting_ && this->peek_next_command_() != EZO_PMP_COMMAND_NONE) { + this->send_next_command_(); + } + + // If we are waiting for something but it isn't ready yet, then return + if (this->is_waiting_ && millis() - this->start_time_ < this->wait_time_) { + return; + } + + // We are waiting for something and it should be ready. + this->read_command_result_(); +} + +void EzoPMP::clear_current_command_() { + this->current_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = false; +} + +void EzoPMP::read_command_result_() { + uint8_t response_buffer[21] = {'\0'}; + + response_buffer[0] = 0; + if (!this->read_bytes_raw(response_buffer, 20)) { + ESP_LOGE(TAG, "read error"); + this->clear_current_command_(); + return; + } + + switch (response_buffer[0]) { + case 254: + return; // keep waiting + case 1: + break; + case 2: + ESP_LOGE(TAG, "device returned a syntax error"); + this->clear_current_command_(); + return; + case 255: + ESP_LOGE(TAG, "device returned no data"); + this->clear_current_command_(); + return; + default: + ESP_LOGE(TAG, "device returned an unknown response: %d", response_buffer[0]); + this->clear_current_command_(); + return; + } + + char first_parameter_buffer[10] = {'\0'}; + char second_parameter_buffer[10] = {'\0'}; + char third_parameter_buffer[10] = {'\0'}; + + first_parameter_buffer[0] = '\0'; + second_parameter_buffer[0] = '\0'; + third_parameter_buffer[0] = '\0'; + + int current_parameter = 1; + + size_t position_in_parameter_buffer = 0; + // some sensors return multiple comma-separated values, terminate string after first one + for (size_t i = 1; i < sizeof(response_buffer) - 1; i++) { + char current_char = response_buffer[i]; + + if (current_char == '\0') { + ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer); + ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer); + ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer); + ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer); + + break; + } + + if (current_char == ',') { + current_parameter++; + position_in_parameter_buffer = 0; + continue; + } + + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + + position_in_parameter_buffer++; + } + + auto parsed_first_parameter = parse_number(first_parameter_buffer); + auto parsed_second_parameter = parse_number(second_parameter_buffer); + auto parsed_third_parameter = parse_number(third_parameter_buffer); + + switch (this->current_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + if (parsed_third_parameter.has_value()) + this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1; + + if (this->is_dosing_) + this->is_dosing_->publish_state(this->is_dosing_flag_); + + if (parsed_second_parameter.has_value() && this->last_volume_requested_) { + this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0)); + } + + if (!this->is_dosing_flag_ && !this->is_paused_flag_) { + // If pump is not paused and not dispensing + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + } + + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) { + this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0)); + } + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57) + if (parsed_second_parameter.has_value() && this->max_flow_rate_) + this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61) + if (parsed_second_parameter.has_value()) + this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1; + + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->total_volume_dosed_) + this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_) + this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65) + if (parsed_second_parameter.has_value() && this->calibration_status_) { + if (parsed_second_parameter.value_or(0) == 1) { + this->calibration_status_->publish_state("Fixed Volume"); + } else if (parsed_second_parameter.value_or(0) == 2) { + this->calibration_status_->publish_state("Volume/Time"); + } else if (parsed_second_parameter.value_or(0) == 3) { + this->calibration_status_->publish_state("Fixed Volume & Volume/Time"); + } else { + this->calibration_status_->publish_state("Uncalibrated"); + } + } + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67) + if (parsed_second_parameter.has_value() && this->pump_voltage_) + this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0)); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE) + this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE); + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS) + this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + this->is_paused_flag_ = false; + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: + ESP_LOGI(TAG, "Arbitrary Command Response: %s", (char *) response_buffer); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + case EZO_PMP_COMMAND_FIND: // Find (page 52) + // Nothing to do here + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->current_command_); + return; + } + + this->clear_current_command_(); +} + +void EzoPMP::send_next_command_() { + int wait_time_for_command = 400; // milliseconds + uint8_t command_buffer[21]; + int command_buffer_length = 0; + + this->pop_next_command_(); // this->next_command will be updated. + + switch (this->next_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + command_buffer_length = sprintf((char *) command_buffer, "D,?"); + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + command_buffer_length = sprintf((char *) command_buffer, "R"); + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: + command_buffer_length = sprintf((char *) command_buffer, "DC,?"); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "P,?"); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "TV,?"); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "ATV,?"); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "Cal,?"); + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: + command_buffer_length = sprintf((char *) command_buffer, "PV,?"); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_FIND: // Find (page 52) + command_buffer_length = sprintf((char *) command_buffer, "Find"); + wait_time_for_command = 60000; // This command will block all updates for a minute + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + command_buffer_length = sprintf((char *) command_buffer, "D,*"); + break; + + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + command_buffer_length = sprintf((char *) command_buffer, "Clear"); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,clear"); + break; + + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + command_buffer_length = sprintf((char *) command_buffer, "P"); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + command_buffer_length = sprintf((char *) command_buffer, "X"); + break; + + // Non-Read commands with parameters + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + command_buffer_length = + sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + command_buffer_length = + sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) + command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command + command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_); + ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->next_command_); + return; + } + + // Send command + ESP_LOGV(TAG, "Sending command to device: %s", (char *) command_buffer); + this->write(command_buffer, command_buffer_length); + + this->current_command_ = this->next_command_; + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = true; + this->start_time_ = millis(); + this->wait_time_ = wait_time_for_command; +} + +void EzoPMP::pop_next_command_() { + if (this->next_command_queue_length_ <= 0) { + ESP_LOGE(TAG, "Tried to dequeue command from empty queue"); + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->next_command_volume_ = 0; + this->next_command_duration_ = 0; + return; + } + + // Read from Head + this->next_command_ = this->next_command_queue_[this->next_command_queue_head_]; + this->next_command_volume_ = this->next_command_volume_queue_[this->next_command_queue_head_]; + this->next_command_duration_ = this->next_command_duration_queue_[this->next_command_queue_head_]; + + // Move positions + next_command_queue_head_++; + if (next_command_queue_head_ >= 10) { + next_command_queue_head_ = 0; + } + + next_command_queue_length_--; +} + +uint16_t EzoPMP::peek_next_command_() { + if (this->next_command_queue_length_ <= 0) { + return EZO_PMP_COMMAND_NONE; + } + + return this->next_command_queue_[this->next_command_queue_head_]; +} + +void EzoPMP::queue_command_(uint16_t command, double volume, int duration, bool should_schedule) { + if (!should_schedule) { + return; + } + + if (this->next_command_queue_length_ >= 10) { + ESP_LOGE(TAG, "Tried to queue command '%d' but queue is full", command); + return; + } + + this->next_command_queue_[this->next_command_queue_last_] = command; + this->next_command_volume_queue_[this->next_command_queue_last_] = volume; + this->next_command_duration_queue_[this->next_command_queue_last_] = duration; + + ESP_LOGV(TAG, "Queue command '%d' in position '%d'", command, next_command_queue_last_); + + // Move positions + next_command_queue_last_++; + if (next_command_queue_last_ >= 10) { + next_command_queue_last_ = 0; + } + + next_command_queue_length_++; +} + +// Actions + +void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); } + +void EzoPMP::dose_continuously() { + this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume_over_time(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::set_calibration_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::clear_total_volume_dosed() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, true); +} + +void EzoPMP::clear_calibration() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_CALIBRATION, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::pause_dosing() { + this->queue_command_(EZO_PMP_COMMAND_PAUSE_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); +} + +void EzoPMP::stop_dosing() { this->queue_command_(EZO_PMP_COMMAND_STOP_DOSING, 0, 0, true); } + +void EzoPMP::change_i2c_address(int address) { + this->queue_command_(EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS, 0, address, true); +} + +void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { + this->arbitrary_command_ = command.c_str(); + this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); +} + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h new file mode 100644 index 0000000000..b41710cd78 --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -0,0 +1,252 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/i2c/i2c.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +namespace esphome { +namespace ezo_pmp { + +class EzoPMP : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void loop() override; + void update() override; + +#ifdef USE_SENSOR + void set_current_volume_dosed(sensor::Sensor *current_volume_dosed) { current_volume_dosed_ = current_volume_dosed; } + void set_total_volume_dosed(sensor::Sensor *total_volume_dosed) { total_volume_dosed_ = total_volume_dosed; } + void set_absolute_total_volume_dosed(sensor::Sensor *absolute_total_volume_dosed) { + absolute_total_volume_dosed_ = absolute_total_volume_dosed; + } + void set_pump_voltage(sensor::Sensor *pump_voltage) { pump_voltage_ = pump_voltage; } + void set_last_volume_requested(sensor::Sensor *last_volume_requested) { + last_volume_requested_ = last_volume_requested; + } + void set_max_flow_rate(sensor::Sensor *max_flow_rate) { max_flow_rate_ = max_flow_rate; } +#endif + +#ifdef USE_BINARY_SENSOR + void set_is_dosing(binary_sensor::BinarySensor *is_dosing) { is_dosing_ = is_dosing; } + void set_is_paused(binary_sensor::BinarySensor *is_paused) { is_paused_ = is_paused; } +#endif + +#ifdef USE_TEXT_SENSOR + void set_dosing_mode(text_sensor::TextSensor *dosing_mode) { dosing_mode_ = dosing_mode; } + void set_calibration_status(text_sensor::TextSensor *calibration_status) { calibration_status_ = calibration_status; } +#endif + + // Actions for EZO-PMP + void find(); + void dose_continuously(); + void dose_volume(double volume); + void dose_volume_over_time(double volume, int duration); + void dose_with_constant_flow_rate(double volume, int duration); + void set_calibration_volume(double volume); + void clear_total_volume_dosed(); + void clear_calibration(); + void pause_dosing(); + void stop_dosing(); + void change_i2c_address(int address); + void exec_arbitrary_command(const std::basic_string &command); + + protected: + uint32_t start_time_ = 0; + uint32_t wait_time_ = 0; + bool is_waiting_ = false; + bool is_first_read_ = true; + + uint16_t next_command_ = 0; + double next_command_volume_ = 0; // might be negative + int next_command_duration_ = 0; + + uint16_t next_command_queue_[10]; + double next_command_volume_queue_[10]; + int next_command_duration_queue_[10]; + int next_command_queue_head_ = 0; + int next_command_queue_last_ = 0; + int next_command_queue_length_ = 0; + + uint16_t current_command_ = 0; + bool is_paused_flag_ = false; + bool is_dosing_flag_ = false; + + const char *arbitrary_command_{nullptr}; + + void send_next_command_(); + void read_command_result_(); + void clear_current_command_(); + void queue_command_(uint16_t command, double volume, int duration, bool should_schedule); + void pop_next_command_(); + uint16_t peek_next_command_(); + +#ifdef USE_SENSOR + sensor::Sensor *current_volume_dosed_{nullptr}; + sensor::Sensor *total_volume_dosed_{nullptr}; + sensor::Sensor *absolute_total_volume_dosed_{nullptr}; + sensor::Sensor *pump_voltage_{nullptr}; + sensor::Sensor *max_flow_rate_{nullptr}; + sensor::Sensor *last_volume_requested_{nullptr}; +#endif + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *is_dosing_{nullptr}; + binary_sensor::BinarySensor *is_paused_{nullptr}; +#endif + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *dosing_mode_{nullptr}; + text_sensor::TextSensor *calibration_status_{nullptr}; +#endif +}; + +// Action Templates +template class EzoPMPFindAction : public Action { + public: + EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->find(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseContinuouslyAction : public Action { + public: + EzoPMPDoseContinuouslyAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_continuously(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeAction : public Action { + public: + EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeOverTimeAction : public Action { + public: + EzoPMPDoseVolumeOverTimeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseWithConstantFlowRateAction : public Action { + public: + EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPSetCalibrationVolumeAction : public Action { + public: + EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearTotalVolumeDispensedAction : public Action { + public: + EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearCalibrationAction : public Action { + public: + EzoPMPClearCalibrationAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_calibration(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPPauseDosingAction : public Action { + public: + EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPStopDosingAction : public Action { + public: + EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPChangeI2CAddressAction : public Action { + public: + EzoPMPChangeI2CAddressAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } + TEMPLATABLE_VALUE(int, address) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPArbitraryCommandAction : public Action { + public: + EzoPMPArbitraryCommandAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + TEMPLATABLE_VALUE(std::string, command) + + protected: + EzoPMP *ezopmp_; +}; + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/sensor.py b/esphome/components/ezo_pmp/sensor.py new file mode 100644 index 0000000000..737985f4c5 --- /dev/null +++ b/esphome/components/ezo_pmp/sensor.py @@ -0,0 +1,104 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + CONF_ID, + UNIT_VOLT, +) + +from . import EzoPMP + + +DEPENDENCIES = ["ezo_pmp"] + +CONF_CURRENT_VOLUME_DOSED = "current_volume_dosed" +CONF_TOTAL_VOLUME_DOSED = "total_volume_dosed" +CONF_ABSOLUTE_TOTAL_VOLUME_DOSED = "absolute_total_volume_dosed" +CONF_PUMP_VOLTAGE = "pump_voltage" +CONF_LAST_VOLUME_REQUESTED = "last_volume_requested" +CONF_MAX_FLOW_RATE = "max_flow_rate" + +UNIT_MILILITER = "ml" +UNIT_MILILITERS_PER_MINUTE = "ml/min" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_CURRENT_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_LAST_VOLUME_REQUESTED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_MAX_FLOW_RATE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITERS_PER_MINUTE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ABSOLUTE_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_PUMP_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_CURRENT_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_CURRENT_VOLUME_DOSED]) + cg.add(parent.set_current_volume_dosed(sens)) + + if CONF_LAST_VOLUME_REQUESTED in config: + sens = await sensor.new_sensor(config[CONF_LAST_VOLUME_REQUESTED]) + cg.add(parent.set_last_volume_requested(sens)) + + if CONF_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_total_volume_dosed(sens)) + + if CONF_ABSOLUTE_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_ABSOLUTE_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_absolute_total_volume_dosed(sens)) + + if CONF_PUMP_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_PUMP_VOLTAGE]) + cg.add(parent.set_pump_voltage(sens)) + + if CONF_MAX_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_MAX_FLOW_RATE]) + cg.add(parent.set_max_flow_rate(sens)) diff --git a/esphome/components/ezo_pmp/text_sensor.py b/esphome/components/ezo_pmp/text_sensor.py new file mode 100644 index 0000000000..f8f133e316 --- /dev/null +++ b/esphome/components/ezo_pmp/text_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_DOSING_MODE = "dosing_mode" +CONF_CALIBRATION_STATUS = "calibration_status" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_DOSING_MODE): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_CALIBRATION_STATUS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_DOSING_MODE in config: + sens = await text_sensor.new_text_sensor(config[CONF_DOSING_MODE]) + cg.add(parent.set_dosing_mode(sens)) + + if CONF_CALIBRATION_STATUS in config: + sens = await text_sensor.new_text_sensor(config[CONF_CALIBRATION_STATUS]) + cg.add(parent.set_calibration_status(sens)) diff --git a/tests/test5.yaml b/tests/test5.yaml index 7fc20c452f..131d2f5b7f 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -165,6 +165,12 @@ binary_sensor: + - platform: ezo_pmp + pump_state: + name: "Pump State" + is_paused: + name: "Is Paused" + tlc5947: data_pin: GPIO12 clock_pin: GPIO14 @@ -220,6 +226,10 @@ esp32_improv: authorized_duration: 1min status_indicator: built_in_led +ezo_pmp: + id: hcl_pump + update_interval: 1s + number: - platform: template name: My template number @@ -440,6 +450,20 @@ sensor: cold_junction: name: Ambient Temperature + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + script: - id: automation_test then: @@ -487,3 +511,33 @@ display: lambda: |- it.print("81818181"); +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? From 138de643a27fc5cd41c0efcdb5432d27626fbb76 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Wed, 19 Oct 2022 06:06:22 +0200 Subject: [PATCH 047/106] Add adc128s102 sensor (#3822) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/adc128s102/__init__.py | 23 ++++++++++++ esphome/components/adc128s102/adc128s102.cpp | 35 +++++++++++++++++++ esphome/components/adc128s102/adc128s102.h | 23 ++++++++++++ .../components/adc128s102/sensor/__init__.py | 35 +++++++++++++++++++ .../adc128s102/sensor/adc128s102_sensor.cpp | 24 +++++++++++++ .../adc128s102/sensor/adc128s102_sensor.h | 29 +++++++++++++++ tests/test3.yaml | 8 +++++ 8 files changed, 178 insertions(+) create mode 100644 esphome/components/adc128s102/__init__.py create mode 100644 esphome/components/adc128s102/adc128s102.cpp create mode 100644 esphome/components/adc128s102/adc128s102.h create mode 100644 esphome/components/adc128s102/sensor/__init__.py create mode 100644 esphome/components/adc128s102/sensor/adc128s102_sensor.cpp create mode 100644 esphome/components/adc128s102/sensor/adc128s102_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 86e6cd978b..688c58b2a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core +esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_mini/* @ncareau diff --git a/esphome/components/adc128s102/__init__.py b/esphome/components/adc128s102/__init__.py new file mode 100644 index 0000000000..c4e9d5831e --- /dev/null +++ b/esphome/components/adc128s102/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ["spi"] +MULTI_CONF = True +CODEOWNERS = ["@DeerMaximum"] + +adc128s102_ns = cg.esphome_ns.namespace("adc128s102") +ADC128S102 = adc128s102_ns.class_("ADC128S102", cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADC128S102), + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/adc128s102/adc128s102.cpp b/esphome/components/adc128s102/adc128s102.cpp new file mode 100644 index 0000000000..c27a354dd3 --- /dev/null +++ b/esphome/components/adc128s102/adc128s102.cpp @@ -0,0 +1,35 @@ +#include "adc128s102.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adc128s102 { + +static const char *const TAG = "adc128s102"; + +float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } + +void ADC128S102::setup() { + ESP_LOGCONFIG(TAG, "Setting up adc128s102"); + this->spi_setup(); +} + +void ADC128S102::dump_config() { + ESP_LOGCONFIG(TAG, "ADC128S102:"); + LOG_PIN(" CS Pin:", this->cs_); +} + +uint16_t ADC128S102::read_data(uint8_t channel) { + uint8_t control = channel << 3; + + this->enable(); + uint8_t adc_primary_byte = this->transfer_byte(control); + uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->disable(); + + uint16_t digital_value = adc_primary_byte << 8 | adc_secondary_byte; + + return digital_value; +} + +} // namespace adc128s102 +} // namespace esphome diff --git a/esphome/components/adc128s102/adc128s102.h b/esphome/components/adc128s102/adc128s102.h new file mode 100644 index 0000000000..bd6b7f7af1 --- /dev/null +++ b/esphome/components/adc128s102/adc128s102.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace adc128s102 { + +class ADC128S102 : public Component, + public spi::SPIDevice { + public: + ADC128S102() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + uint16_t read_data(uint8_t channel); +}; + +} // namespace adc128s102 +} // namespace esphome diff --git a/esphome/components/adc128s102/sensor/__init__.py b/esphome/components/adc128s102/sensor/__init__.py new file mode 100644 index 0000000000..3ab6fc4c38 --- /dev/null +++ b/esphome/components/adc128s102/sensor/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import CONF_ID, CONF_CHANNEL + +from .. import adc128s102_ns, ADC128S102 + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["adc128s102"] + +ADC128S102Sensor = adc128s102_ns.class_( + "ADC128S102Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, +) +CONF_ADC128S102_ID = "adc128s102_id" + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ADC128S102Sensor), + cv.GenerateID(CONF_ADC128S102_ID): cv.use_id(ADC128S102), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_CHANNEL], + ) + await cg.register_parented(var, config[CONF_ADC128S102_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp new file mode 100644 index 0000000000..03ce31d3cb --- /dev/null +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -0,0 +1,24 @@ +#include "adc128s102_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace adc128s102 { + +static const char *const TAG = "adc128s102.sensor"; + +ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} + +float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void ADC128S102Sensor::dump_config() { + LOG_SENSOR("", "ADC128S102 Sensor", this); + ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); + LOG_UPDATE_INTERVAL(this); +} + +float ADC128S102Sensor::sample() { return this->parent_->read_data(this->channel_); } +void ADC128S102Sensor::update() { this->publish_state(this->sample()); } + +} // namespace adc128s102 +} // namespace esphome diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h new file mode 100644 index 0000000000..234500c2f4 --- /dev/null +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include "../adc128s102.h" + +namespace esphome { +namespace adc128s102 { + +class ADC128S102Sensor : public PollingComponent, + public Parented, + public sensor::Sensor, + public voltage_sampler::VoltageSampler { + public: + ADC128S102Sensor(uint8_t channel); + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t channel_; +}; +} // namespace adc128s102 +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 8fc66f4918..8150f43e02 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -789,6 +789,11 @@ sensor: voltage: name: Voltage update_interval: 60s + + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 + time: - platform: homeassistant @@ -1540,3 +1545,6 @@ cd74hc4067: pin_s1: GPIO13 pin_s2: GPIO14 pin_s3: GPIO15 + +adc128s102: + cs_pin: GPIO12 From d7576f67e85701d2ec35786b1c4c25432b7e31d1 Mon Sep 17 00:00:00 2001 From: hagak <9702199+hagak@users.noreply.github.com> Date: Wed, 19 Oct 2022 03:29:22 -0400 Subject: [PATCH 048/106] Added component Daikin BRC to support ceiling cassette heatpumps (#3743) --- CODEOWNERS | 1 + esphome/components/daikin_brc/__init__.py | 1 + esphome/components/daikin_brc/climate.py | 24 ++ esphome/components/daikin_brc/daikin_brc.cpp | 273 +++++++++++++++++++ esphome/components/daikin_brc/daikin_brc.h | 82 ++++++ tests/test1.yaml | 3 + 6 files changed, 384 insertions(+) create mode 100644 esphome/components/daikin_brc/__init__.py create mode 100644 esphome/components/daikin_brc/climate.py create mode 100644 esphome/components/daikin_brc/daikin_brc.cpp create mode 100644 esphome/components/daikin_brc/daikin_brc.h diff --git a/CODEOWNERS b/CODEOWNERS index 688c58b2a4..78624ecdbe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -58,6 +58,7 @@ esphome/components/cse7761/* @berfenger esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx esphome/components/dac7678/* @NickB1 +esphome/components/daikin_brc/* @hagak esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter diff --git a/esphome/components/daikin_brc/__init__.py b/esphome/components/daikin_brc/__init__.py new file mode 100644 index 0000000000..69002c015f --- /dev/null +++ b/esphome/components/daikin_brc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@hagak"] diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py new file mode 100644 index 0000000000..3468b6533c --- /dev/null +++ b/esphome/components/daikin_brc/climate.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") +DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DaikinBrcClimate), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp new file mode 100644 index 0000000000..6683d70f80 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -0,0 +1,273 @@ +#include "daikin_brc.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace daikin_brc { + +static const char *const TAG = "daikin_brc.climate"; + +void DaikinBrcClimate::control(const climate::ClimateCall &call) { + this->mode_button_ = 0x00; + if (call.get_mode().has_value()) { + // Need to determine if this is call due to Mode button pressed so that we can set the Mode button byte + this->mode_button_ = DAIKIN_BRC_IR_MODE_BUTTON; + } + ClimateIR::control(call); +} + +void DaikinBrcClimate::transmit_state() { + uint8_t remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE] = {0x11, 0xDA, 0x17, 0x18, 0x04, 0x00, 0x1E, 0x11, + 0xDA, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00}; + + remote_state[12] = this->alt_mode_(); + remote_state[13] = this->mode_button_; + remote_state[14] = this->operation_mode_(); + remote_state[17] = this->temperature_(); + remote_state[18] = this->fan_speed_swing_(); + + // Calculate checksum + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1; i++) { + remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_BRC_IR_FREQUENCY); + + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(DAIKIN_BRC_MESSAGE_SPACE); + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinBrcClimate::alt_mode_() { + uint8_t alt_mode = 0x00; + switch (this->mode) { + case climate::CLIMATE_MODE_DRY: + alt_mode = 0x23; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + alt_mode = 0x63; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + default: + alt_mode = 0x73; + break; + } + return alt_mode; +} + +uint8_t DaikinBrcClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_BRC_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_BRC_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_BRC_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_BRC_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DAIKIN_BRC_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_BRC_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_BRC_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t DaikinBrcClimate::fan_speed_swing_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_BRC_FAN_1; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_BRC_FAN_2; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_BRC_FAN_3; + break; + default: + fan_speed = DAIKIN_BRC_FAN_1; + } + + // If swing is enabled switch first 4 bits to 1111 + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + fan_speed |= DAIKIN_BRC_IR_SWING_ON; + break; + default: + fan_speed |= DAIKIN_BRC_IR_SWING_OFF; + break; + } + return fan_speed; +} + +uint8_t DaikinBrcClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + if (this->fahrenheit_) { + return DAIKIN_BRC_IR_DRY_FAN_TEMP_F; + } + return DAIKIN_BRC_IR_DRY_FAN_TEMP_C; + case climate::CLIMATE_MODE_HEAT_COOL: + default: + uint8_t temperature; + // Temperature in remote is in F + if (this->fahrenheit_) { + temperature = (uint8_t) roundf( + clamp(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F)); + } else { + temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1; + } + return temperature; + } +} + +bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) { + ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum); + return false; + } + + uint8_t mode = frame[7]; + if (mode & DAIKIN_BRC_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_BRC_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_BRC_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_BRC_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_BRC_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DAIKIN_BRC_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + + uint8_t temperature = frame[10]; + float temperature_c; + if (this->fahrenheit_) { + temperature_c = clamp(((temperature - 32) / 1.8), DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C); + } else { + temperature_c = (temperature >> 1) + 9; + } + + this->target_temperature = temperature_c; + + uint8_t fan_mode = frame[11]; + uint8_t swing_mode = frame[11]; + switch (swing_mode & 0xF) { + case DAIKIN_BRC_IR_SWING_ON: + this->swing_mode = climate::CLIMATE_SWING_BOTH; + break; + case DAIKIN_BRC_IR_SWING_OFF: + this->swing_mode = climate::CLIMATE_SWING_OFF; + break; + } + + switch (fan_mode & 0xF0) { + case DAIKIN_BRC_FAN_1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_BRC_FAN_2: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_BRC_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + } + this->publish_state(); + return true; +} + +bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) + return false; + } else if (pos == 1) { + // frame header + if (byte != 0xDA) + return false; + } else if (pos == 2) { + // frame header + if (byte != 0x17) + return false; + } else if (pos == 3) { + // frame header + if (byte != 0x18) + return false; + } else if (pos == 4) { + // frame type + if (byte != 0x00) + return false; + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace daikin_brc +} // namespace esphome diff --git a/esphome/components/daikin_brc/daikin_brc.h b/esphome/components/daikin_brc/daikin_brc.h new file mode 100644 index 0000000000..bdc6384809 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin_brc { + +// Values for Daikin BRC4CXXX IR Controllers +// Temperature +const uint8_t DAIKIN_BRC_TEMP_MIN_F = 60; // fahrenheit +const uint8_t DAIKIN_BRC_TEMP_MAX_F = 90; // fahrenheit +const float DAIKIN_BRC_TEMP_MIN_C = (DAIKIN_BRC_TEMP_MIN_F - 32) / 1.8; // fahrenheit +const float DAIKIN_BRC_TEMP_MAX_C = (DAIKIN_BRC_TEMP_MAX_F - 32) / 1.8; // fahrenheit + +// Modes +const uint8_t DAIKIN_BRC_MODE_AUTO = 0x30; +const uint8_t DAIKIN_BRC_MODE_COOL = 0x20; +const uint8_t DAIKIN_BRC_MODE_HEAT = 0x10; +const uint8_t DAIKIN_BRC_MODE_DRY = 0x70; +const uint8_t DAIKIN_BRC_MODE_FAN = 0x00; +const uint8_t DAIKIN_BRC_MODE_OFF = 0x00; +const uint8_t DAIKIN_BRC_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_BRC_FAN_1 = 0x10; +const uint8_t DAIKIN_BRC_FAN_2 = 0x30; +const uint8_t DAIKIN_BRC_FAN_3 = 0x50; +const uint8_t DAIKIN_BRC_FAN_AUTO = 0xA0; + +// IR Transmission +const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_BRC_HEADER_MARK = 5070; +const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140; +const uint32_t DAIKIN_BRC_BIT_MARK = 370; +const uint32_t DAIKIN_BRC_ONE_SPACE = 1780; +const uint32_t DAIKIN_BRC_ZERO_SPACE = 710; +const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410; + +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_C = (17 - 9) * 2; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_SWING_ON = 0x5; +const uint8_t DAIKIN_BRC_IR_SWING_OFF = 0x6; +const uint8_t DAIKIN_BRC_IR_MODE_BUTTON = 0x4; // This is set after a mode action + +// State Frame size +const uint8_t DAIKIN_BRC_STATE_FRAME_SIZE = 15; +// Preamble size +const uint8_t DAIKIN_BRC_PREAMBLE_SIZE = 7; +// Transmit Frame size - includes a preamble +const uint8_t DAIKIN_BRC_TRANSMIT_FRAME_SIZE = DAIKIN_BRC_PREAMBLE_SIZE + DAIKIN_BRC_STATE_FRAME_SIZE; + +class DaikinBrcClimate : public climate_ir::ClimateIR { + public: + DaikinBrcClimate() + : climate_ir::ClimateIR(DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C, 0.5f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH}) {} + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + uint8_t mode_button_ = 0x00; + // Capture if the MODE was changed + void control(const climate::ClimateCall &call) override; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t alt_mode_(); + uint8_t operation_mode_(); + uint8_t fan_speed_swing_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); + bool fahrenheit_{false}; +}; + +} // namespace daikin_brc +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 82cdac4623..b286c37deb 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1862,6 +1862,9 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: daikin_brc + name: Daikin BRC Climate + use_fahrenheit: true - platform: delonghi name: Delonghi Climate - platform: yashima From a21c3e8e2db3dddd55c9a56ee348a6a76a6daeb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 12:49:11 +1300 Subject: [PATCH 049/106] Bump platformio from 6.0.2 to 6.1.4 (#3711) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f4652bc513..05b518ca74 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -46,7 +46,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==6.0.2 \ + platformio==6.1.4 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ diff --git a/requirements.txt b/requirements.txt index 00e6e0ba67..af7ac0cab4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ tornado==6.1 tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.0.2 # When updating platformio, also update Dockerfile +platformio==6.1.4 # When updating platformio, also update Dockerfile esptool==3.3.1 click==8.1.3 esphome-dashboard==20221007.0 From e87edcc77a4e4c83624cad270eebdd19b30669d5 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Thu, 20 Oct 2022 05:39:34 +0200 Subject: [PATCH 050/106] Add API interface to request a complete device config as JSON. (#3911) Co-authored-by: Paulus Schoutsen --- esphome/dashboard/dashboard.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 1a51f3056f..0367101023 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -29,6 +29,7 @@ import tornado.web import tornado.websocket from esphome import const, platformio_api, util, yaml_util +from esphome.core import EsphomeError from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, @@ -927,6 +928,27 @@ class SecretKeysRequestHandler(BaseHandler): self.write(json.dumps(secret_keys)) +class JsonConfigRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + filename = settings.rel_path(configuration) + if not os.path.isfile(filename): + self.send_error(404) + return + + try: + content = yaml_util.load_yaml(filename, clear_secrets=False) + json_content = json.dumps( + content, default=lambda o: {"__type": str(type(o)), "repr": repr(o)} + ) + self.set_header("content-type", "application/json") + self.write(json_content) + except EsphomeError as err: + _LOGGER.warning("Error translating file %s to JSON: %s", filename, err) + self.send_error(500) + + def get_base_frontend_path(): if ENV_DEV not in os.environ: import esphome_dashboard @@ -1031,6 +1053,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}devices", ListDevicesHandler), (f"{rel}import", ImportRequestHandler), (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}json-config", JsonConfigRequestHandler), (f"{rel}rename", EsphomeRenameHandler), (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), ], From 6153bcc6ad818c8ece3fc149614d7aa391bd483f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:50:39 +1300 Subject: [PATCH 051/106] Initial Support for RP2040 platform (#3284) Co-authored-by: Paulus Schoutsen --- .github/workflows/ci.yml | 4 + CODEOWNERS | 2 + esphome/__main__.py | 72 ++++--- esphome/components/logger/__init__.py | 17 +- esphome/components/logger/logger.cpp | 6 +- esphome/components/logger/logger.h | 12 +- esphome/components/md5/md5.cpp | 15 +- esphome/components/md5/md5.h | 5 + esphome/components/mdns/mdns_component.cpp | 3 + esphome/components/mdns/mdns_rp2040.cpp | 40 ++++ esphome/components/ota/__init__.py | 5 +- .../ota/ota_backend_arduino_rp2040.cpp | 55 +++++ .../ota/ota_backend_arduino_rp2040.h | 27 +++ esphome/components/ota/ota_component.cpp | 4 + esphome/components/ota/ota_component.h | 1 + esphome/components/rp2040/__init__.py | 157 ++++++++++++++ esphome/components/rp2040/boards.py | 7 + esphome/components/rp2040/const.py | 6 + esphome/components/rp2040/core.cpp | 32 +++ esphome/components/rp2040/core.h | 14 ++ esphome/components/rp2040/gpio.cpp | 103 +++++++++ esphome/components/rp2040/gpio.h | 38 ++++ esphome/components/rp2040/gpio.py | 91 ++++++++ esphome/components/rp2040/preferences.cpp | 49 +++++ esphome/components/rp2040/preferences.h | 13 ++ esphome/components/rp2040_pwm/__init__.py | 0 esphome/components/rp2040_pwm/output.py | 55 +++++ esphome/components/rp2040_pwm/rp2040_pwm.cpp | 45 ++++ esphome/components/rp2040_pwm/rp2040_pwm.h | 57 +++++ esphome/components/socket/__init__.py | 1 + esphome/components/socket/headers.h | 2 +- esphome/components/spi/__init__.py | 4 +- esphome/components/spi/spi.h | 20 ++ esphome/components/wifi/__init__.py | 4 +- esphome/components/wifi/wifi_component.cpp | 2 + esphome/components/wifi/wifi_component.h | 17 ++ .../components/wifi/wifi_component_pico_w.cpp | 204 ++++++++++++++++++ esphome/config_validation.py | 4 + esphome/const.py | 3 +- esphome/core/__init__.py | 4 + esphome/core/helpers.cpp | 8 + esphome/dashboard/dashboard.py | 28 ++- esphome/storage_json.py | 6 +- esphome/wizard.py | 20 +- platformio.ini | 25 +++ script/ci-custom.py | 1 + tests/README.md | 1 + tests/dummy_main.cpp | 3 +- tests/test6.yaml | 39 ++++ 49 files changed, 1270 insertions(+), 61 deletions(-) create mode 100644 esphome/components/mdns/mdns_rp2040.cpp create mode 100644 esphome/components/ota/ota_backend_arduino_rp2040.cpp create mode 100644 esphome/components/ota/ota_backend_arduino_rp2040.h create mode 100644 esphome/components/rp2040/__init__.py create mode 100644 esphome/components/rp2040/boards.py create mode 100644 esphome/components/rp2040/const.py create mode 100644 esphome/components/rp2040/core.cpp create mode 100644 esphome/components/rp2040/core.h create mode 100644 esphome/components/rp2040/gpio.cpp create mode 100644 esphome/components/rp2040/gpio.h create mode 100644 esphome/components/rp2040/gpio.py create mode 100644 esphome/components/rp2040/preferences.cpp create mode 100644 esphome/components/rp2040/preferences.h create mode 100644 esphome/components/rp2040_pwm/__init__.py create mode 100644 esphome/components/rp2040_pwm/output.py create mode 100644 esphome/components/rp2040_pwm/rp2040_pwm.cpp create mode 100644 esphome/components/rp2040_pwm/rp2040_pwm.h create mode 100644 esphome/components/wifi/wifi_component_pico_w.cpp create mode 100644 tests/test6.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671b74d118..8e09fef069 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,10 @@ jobs: file: tests/test5.yaml name: Test tests/test5.yaml pio_cache_key: test5 + - id: test + file: tests/test6.yaml + name: Test tests/test6.yaml + pio_cache_key: test6 - id: pytest name: Run pytest - id: clang-format diff --git a/CODEOWNERS b/CODEOWNERS index 78624ecdbe..ac793e19af 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -184,6 +184,8 @@ esphome/components/rc522_spi/* @glmnet esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz +esphome/components/rp2040/* @jesserockz +esphome/components/rp2040_pwm/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny diff --git a/esphome/__main__.py b/esphome/__main__.py index c336336f18..cf2d161d04 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -4,6 +4,7 @@ import logging import os import re import sys +import time from datetime import datetime from esphome import const, writer, yaml_util @@ -22,6 +23,9 @@ from esphome.const import ( CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, CONF_SUBSTITUTIONS, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -101,11 +105,11 @@ def run_miniterm(config, port): if CONF_LOGGER not in config: _LOGGER.info("Logger is not enabled. Not starting UART logs.") - return + return 1 baud_rate = config["logger"][CONF_BAUD_RATE] if baud_rate == 0: _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") - return + return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) backtrace_state = False @@ -119,25 +123,34 @@ def run_miniterm(config, port): ser.dtr = False ser.rts = False - with ser: - while True: - try: - raw = ser.readline() - except serial.SerialException: - _LOGGER.error("Serial port closed!") - return - line = ( - raw.replace(b"\r", b"") - .replace(b"\n", b"") - .decode("utf8", "backslashreplace") - ) - time = datetime.now().time().strftime("[%H:%M:%S]") - message = time + line - safe_print(message) + tries = 0 + while tries < 5: + try: + with ser: + while True: + try: + raw = ser.readline() + except serial.SerialException: + _LOGGER.error("Serial port closed!") + return 0 + line = ( + raw.replace(b"\r", b"") + .replace(b"\n", b"") + .decode("utf8", "backslashreplace") + ) + time_str = datetime.now().time().strftime("[%H:%M:%S]") + message = time_str + line + safe_print(message) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) + except serial.SerialException: + tries += 1 + time.sleep(1) + if tries >= 5: + _LOGGER.error("Could not connect to serial port %s", port) + return 1 def wrap_to_code(name, comp): @@ -258,9 +271,21 @@ def upload_using_esptool(config, port): def upload_program(config, args, host): - # if upload is to a serial port use platformio, otherwise assume ota if get_port_type(host) == "SERIAL": - return upload_using_esptool(config, host) + if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): + return upload_using_esptool(config, host) + + if CORE.target_platform in (PLATFORM_RP2040): + from esphome import platformio_api + + upload_args = ["-t", "upload"] + if args.device is not None: + upload_args += ["--upload-port", args.device] + return platformio_api.run_platformio_cli_run( + config, CORE.verbose, *upload_args + ) + + return 1 # Unknown target platform from esphome import espota2 @@ -280,8 +305,7 @@ def show_logs(config, args, port): if "logger" not in config: raise EsphomeError("Logger is not configured!") if get_port_type(port) == "SERIAL": - run_miniterm(config, port) - return 0 + return run_miniterm(config, port) if get_port_type(port) == "NETWORK" and "api" in config: from esphome.components.api.client import run_logs diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 43d87bcefe..333e109379 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -77,6 +77,8 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +UART_SELECTION_RP2040 = [UART0, UART1] + HARDWARE_UART_TO_UART_SELECTION = { UART0: logger_ns.UART_SELECTION_UART0, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, @@ -106,6 +108,8 @@ def uart_selection(value): return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) + if CORE.is_rp2040: + return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value) raise NotImplementedError @@ -158,12 +162,13 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(90.0) async def to_code(config): baud_rate = config[CONF_BAUD_RATE] - rhs = Logger.new( - baud_rate, - config[CONF_TX_BUFFER_SIZE], - HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]], - ) - log = cg.Pvariable(config[CONF_ID], rhs) + log = cg.new_Pvariable(config[CONF_ID], baud_rate, config[CONF_TX_BUFFER_SIZE]) + if CONF_HARDWARE_UART in config: + cg.add( + log.set_uart_selection( + HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] + ) + ) cg.add(log.pre_setup()) for tag, level in config[CONF_LOGS].items(): diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index c97677c887..271f99ba58 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -148,8 +148,7 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { this->log_callback_.call(level, tag, msg); } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) - : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) { +Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { // add 1 to buffer size for null terminator this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT } @@ -270,6 +269,9 @@ const char *const UART_SELECTIONS[] = { #endif // USE_ESP32 #ifdef USE_ESP8266 const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; +#endif +#ifdef USE_RP2040 +const char *const UART_SELECTIONS[] = {"UART0", "UART1"}; #endif // USE_ESP8266 void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 307c13a91f..0251faf987 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -7,8 +7,15 @@ #include #ifdef USE_ARDUINO +#if defined(USE_ESP8266) || defined(USE_ESP32) #include -#endif +#endif // USE_ESP8266 || USE_ESP32 +#ifdef USE_RP2040 +#include +#include +#endif // USE_RP2040 +#endif // USE_ARDUINO + #ifdef USE_ESP_IDF #include #endif @@ -44,7 +51,7 @@ enum UARTSelection { class Logger : public Component { public: - explicit Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart); + explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); @@ -56,6 +63,7 @@ class Logger : public Component { uart_port_t get_uart_num() const { return uart_num_; } #endif + void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 8d4bac1fd2..620b6749f3 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -6,7 +6,7 @@ namespace esphome { namespace md5 { -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2040) void MD5Digest::init() { memset(this->digest_, 0, 16); MD5Init(&this->ctx_); @@ -15,7 +15,7 @@ void MD5Digest::init() { void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); } void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } -#endif // USE_ARDUINO +#endif // USE_ARDUINO && !USE_RP2040 #ifdef USE_ESP_IDF void MD5Digest::init() { @@ -28,6 +28,17 @@ void MD5Digest::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&this- void MD5Digest::calculate() { esp_rom_md5_final(this->digest_, &this->ctx_); } #endif // USE_ESP_IDF +#ifdef USE_RP2040 +void MD5Digest::init() { + memset(this->digest_, 0, 16); + br_md5_init(&this->ctx_); +} + +void MD5Digest::add(const uint8_t *data, size_t len) { br_md5_update(&this->ctx_, data, len); } + +void MD5Digest::calculate() { br_md5_out(&this->ctx_, this->digest_); } +#endif // USE_RP2040 + void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); } void MD5Digest::get_hex(char *output) { diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index a9628c9242..738a312267 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -17,6 +17,11 @@ #define MD5_CTX_TYPE md5_context_t #endif +#ifdef USE_RP2040 +#include +#define MD5_CTX_TYPE br_md5_context +#endif + namespace esphome { namespace md5 { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index e1fcf320ed..a3f38322b3 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -38,6 +38,9 @@ void MDNSComponent::compile_records_() { #endif #ifdef USE_ESP32 platform = "ESP32"; +#endif +#ifdef USE_RP2040 + platform = "RP2040"; #endif if (platform != nullptr) { service.txt_records.push_back({"platform", platform}); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp new file mode 100644 index 0000000000..b153ececcc --- /dev/null +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -0,0 +1,40 @@ +#ifdef USE_RP2040 + +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include "esphome/core/log.h" +#include "mdns_component.h" + +namespace esphome { +namespace mdns { + +void MDNSComponent::setup() { + this->compile_records_(); + + network::IPAddress addr = network::get_ip_address(); + // MDNS.begin(this->hostname_.c_str(), (uint32_t) addr); + + // for (const auto &service : this->services_) { + // // Strip the leading underscore from the proto and service_type. While it is + // // part of the wire protocol to have an underscore, and for example ESP-IDF + // // expects the underscore to be there, the ESP8266 implementation always adds + // // the underscore itself. + // auto *proto = service.proto.c_str(); + // while (*proto == '_') { + // proto++; + // } + // auto *service_type = service.service_type.c_str(); + // while (*service_type == '_') { + // service_type++; + // } + // MDNS.addService(service_type, proto, service.port); + // for (const auto &record : service.txt_records) { + // MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str()); + // } + // } +} + +} // namespace mdns +} // namespace esphome + +#endif diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 1bc4012ce2..32ea1fd363 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(OTAComponent), cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, - cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port, + cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232, rp2040=2040): cv.port, cv.Optional(CONF_PASSWORD): cv.string, cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" @@ -94,6 +94,9 @@ async def to_code(config): if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) + if CORE.is_rp2040 and CORE.using_arduino: + cg.add_library("Updater", None) + use_state_callback = False for conf in config.get(CONF_ON_STATE_CHANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp new file mode 100644 index 0000000000..5a46b8f07e --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -0,0 +1,55 @@ +#include "esphome/core/defines.h" +#ifdef USE_ARDUINO +#ifdef USE_RP2040 + +#include "esphome/components/rp2040/preferences.h" +#include "ota_backend.h" +#include "ota_backend_arduino_rp2040.h" +#include "ota_component.h" + +#include + +namespace esphome { +namespace ota { + +OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { + bool ret = Update.begin(image_size, U_FLASH); + if (ret) { + return OTA_RESPONSE_OK; + } + + uint8_t error = Update.getError(); + if (error == UPDATE_ERROR_BOOTSTRAP) + return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; + if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) + return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; + if (error == UPDATE_ERROR_FLASH_CONFIG) + return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; + if (error == UPDATE_ERROR_SPACE) + return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE; + return OTA_RESPONSE_ERROR_UNKNOWN; +} + +void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } + +OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { + size_t written = Update.write(data, len); + if (written != len) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + return OTA_RESPONSE_OK; +} + +OTAResponseTypes ArduinoRP2040OTABackend::end() { + if (!Update.end()) + return OTA_RESPONSE_ERROR_UPDATE_END; + return OTA_RESPONSE_OK; +} + +void ArduinoRP2040OTABackend::abort() { Update.end(); } + +} // namespace ota +} // namespace esphome + +#endif // USE_RP2040 +#endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h new file mode 100644 index 0000000000..5aa2ec9435 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -0,0 +1,27 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ARDUINO +#ifdef USE_RP2040 + +#include "esphome/core/macros.h" +#include "ota_backend.h" +#include "ota_component.h" + +namespace esphome { +namespace ota { + +class ArduinoRP2040OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + bool supports_compression() override { return false; } +}; + +} // namespace ota +} // namespace esphome + +#endif // USE_RP2040 +#endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index a02d64cd08..1f1ecd9867 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -2,6 +2,7 @@ #include "ota_backend.h" #include "ota_backend_arduino_esp32.h" #include "ota_backend_arduino_esp8266.h" +#include "ota_backend_arduino_rp2040.h" #include "ota_backend_esp_idf.h" #include "esphome/core/log.h" @@ -35,6 +36,9 @@ std::unique_ptr make_ota_backend() { #ifdef USE_ESP_IDF return make_unique(); #endif // USE_ESP_IDF +#ifdef USE_RP2040 + return make_unique(); +#endif // USE_RP2040 } OTAComponent::OTAComponent() { global_ota_component = this; } diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index 9a1c92f727..50d095be6c 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -33,6 +33,7 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137, OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138, OTA_RESPONSE_ERROR_MD5_MISMATCH = 139, + OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140, OTA_RESPONSE_ERROR_UNKNOWN = 255, }; diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py new file mode 100644 index 0000000000..d526228f41 --- /dev/null +++ b/esphome/components/rp2040/__init__.py @@ -0,0 +1,157 @@ +import logging + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_BOARD, + CONF_FRAMEWORK, + CONF_SOURCE, + CONF_VERSION, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE, coroutine_with_priority + +from .const import KEY_BOARD, KEY_RP2040, rp2040_ns + +# force import gpio to register pin schema +from .gpio import rp2040_pin_to_code # noqa + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@jesserockz"] +AUTO_LOAD = [] + + +def set_core_data(config): + CORE.data[KEY_RP2040] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "rp2040" + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + config[CONF_FRAMEWORK][CONF_VERSION] + ) + CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD] + + return config + + +def _format_framework_arduino_version(ver: cv.Version) -> str: + # format the given arduino (https://github.com/earlephilhower/arduino-pico/releases) version to + # a PIO earlephilhower/framework-arduinopico value + # List of package versions: https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico + return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + +# NOTE: Keep this in mind when updating the recommended version: +# * The new version needs to be thoroughly validated before changing the +# recommended version as otherwise a bunch of devices could be bricked +# * For all constants below, update platformio.ini (in this repo) +# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository + +# The default/recommended arduino framework version +# - https://github.com/earlephilhower/arduino-pico/releases +# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 4, 0) + +# The platformio/raspberrypi version to use for arduino frameworks +# - https://github.com/platformio/platform-raspberrypi/releases +# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi +ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0) + + +def _arduino_check_versions(value): + value = value.copy() + lookups = { + "dev": (cv.Version(2, 4, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(2, 4, 0), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), + } + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) + + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + ) + + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected Arduino framework version is not the recommended one." + ) + + return value + + +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/raspberrypi @ {value}" + except cv.Invalid: + return value + + +CONF_PLATFORM_VERSION = "platform_version" + +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + } + ), + _arduino_check_versions, +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, + } + ), + set_core_data, +) + + +@coroutine_with_priority(1000) +async def to_code(config): + cg.add(rp2040_ns.setup_preferences()) + + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_RP2040") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "RP2040") + + conf = config[CONF_FRAMEWORK] + cg.add_platformio_option("framework", "arduino") + cg.add_build_flag("-DUSE_ARDUINO") + cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO") + # cg.add_build_flag("-DPICO_BOARD=pico_w") + cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + cg.add_platformio_option( + "platform_packages", + [f"earlephilhower/framework-arduinopico @ {conf[CONF_SOURCE]}"], + ) + + cg.add_platformio_option("board_build.core", "earlephilhower") + cg.add_platformio_option("board_build.filesystem_size", "0.5m") + + ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + cg.add_define( + "USE_ARDUINO_VERSION_CODE", + cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), + ) diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py new file mode 100644 index 0000000000..be3d05369a --- /dev/null +++ b/esphome/components/rp2040/boards.py @@ -0,0 +1,7 @@ +RP2040_BASE_PINS = {} + +RP2040_BOARD_PINS = { + "pico": {"LED": 25}, + "rpipico": "pico", + "rpipicow": {}, +} diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py new file mode 100644 index 0000000000..e09016ca31 --- /dev/null +++ b/esphome/components/rp2040/const.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +KEY_BOARD = "board" +KEY_RP2040 = "rp2040" + +rp2040_ns = cg.esphome_ns.namespace("rp2040") diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp new file mode 100644 index 0000000000..2a1ce5a4d3 --- /dev/null +++ b/esphome/components/rp2040/core.cpp @@ -0,0 +1,32 @@ +#ifdef USE_RP2040 + +#include "core.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include "hardware/watchdog.h" + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::yield(); } +uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +uint32_t IRAM_ATTR HOT micros() { return ::micros(); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +void arch_restart() { + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} +void arch_init() { watchdog_enable(0x7fffff, false); } +void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } + +uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } +uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/core.h b/esphome/components/rp2040/core.h new file mode 100644 index 0000000000..92fc4f824e --- /dev/null +++ b/esphome/components/rp2040/core.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_RP2040 + +#include +#include + +extern "C" unsigned long ulMainGetRunTimeCounterValue(); + +namespace esphome { +namespace rp2040 {} // namespace rp2040 +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp new file mode 100644 index 0000000000..e32b93b5c2 --- /dev/null +++ b/esphome/components/rp2040/gpio.cpp @@ -0,0 +1,103 @@ +#ifdef USE_RP2040 + +#include "gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rp2040 { + +static const char *const TAG = "rp2040"; + +static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { + if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN; + // } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + // return OpenDrain; + } else { + return 0; + } +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin RP2040GPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void RP2040GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + PinStatus arduino_mode = LOW; + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + arduino_mode = inverted_ ? FALLING : RISING; + break; + case gpio::INTERRUPT_FALLING_EDGE: + arduino_mode = inverted_ ? RISING : FALLING; + break; + case gpio::INTERRUPT_ANY_EDGE: + arduino_mode = CHANGE; + break; + case gpio::INTERRUPT_LOW_LEVEL: + arduino_mode = inverted_ ? HIGH : LOW; + break; + case gpio::INTERRUPT_HIGH_LEVEL: + arduino_mode = inverted_ ? LOW : HIGH; + break; + } + + attachInterrupt(pin_, func, arduino_mode, arg); +} +void RP2040GPIOPin::pin_mode(gpio::Flags flags) { + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT +} + +std::string RP2040GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool RP2040GPIOPin::digital_read() { + return bool(digitalRead(pin_)) != inverted_; // NOLINT +} +void RP2040GPIOPin::digital_write(bool value) { + digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT +} +void RP2040GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } + +} // namespace rp2040 + +using namespace rp2040; + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + auto *arg = reinterpret_cast(arg_); + digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + // TODO: implement + // auto *arg = reinterpret_cast(arg_); + // GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); +} +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT +} + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h new file mode 100644 index 0000000000..ef9500d5dd --- /dev/null +++ b/esphome/components/rp2040/gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_RP2040 + +#include +#include "esphome/core/hal.h" + +namespace esphome { +namespace rp2040 { + +class RP2040GPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + void setup() override { pin_mode(flags_); } + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace rp2040 +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py new file mode 100644 index 0000000000..4bc6dbee1c --- /dev/null +++ b/esphome/components/rp2040/gpio.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +from esphome.core import CORE +from esphome import pins + +from . import boards +from .const import KEY_BOARD, KEY_RP2040, rp2040_ns + +RP2040GPIOPin = rp2040_ns.class_("RP2040GPIOPin", cg.InternalGPIOPin) + + +def _lookup_pin(value): + board = CORE.data[KEY_RP2040][KEY_BOARD] + board_pins = boards.RP2040_BOARD_PINS.get(board, {}) + + while isinstance(board_pins, str): + board_pins = boards.RP2040_BOARD_PINS[board_pins] + + if value in board_pins: + return board_pins[value] + if value in boards.RP2040_BASE_PINS: + return boards.RP2040_BASE_PINS[value] + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return _lookup_pin(value) + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > 29: + raise cv.Invalid(f"RP2040: Invalid pin number: {value}") + return value + + +CONF_ANALOG = "analog" + +RP2040_PIN_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RP2040GPIOPin), + cv.Required(CONF_NUMBER): validate_gpio_pin, + cv.Optional(CONF_MODE, default={}): cv.Schema( + { + cv.Optional(CONF_ANALOG, default=False): cv.boolean, + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } + ) +) + + +@pins.PIN_SCHEMA_REGISTRY.register("rp2040", RP2040_PIN_SCHEMA) +async def rp2040_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp new file mode 100644 index 0000000000..58ec57df2a --- /dev/null +++ b/esphome/components/rp2040/preferences.cpp @@ -0,0 +1,49 @@ +#ifdef USE_RP2040 + +#include "preferences.h" + +#include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace rp2040 { + +static const char *const TAG = "rp2040.preferences"; + +class RP2040PreferenceBackend : public ESPPreferenceBackend { + public: + bool save(const uint8_t *data, size_t len) override { return true; } + bool load(uint8_t *data, size_t len) override { return false; } +}; + +class RP2040Preferences : public ESPPreferences { + public: + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + return ESPPreferenceObject(pref); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { + auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + return ESPPreferenceObject(pref); + } + + bool sync() override { return true; } + + bool reset() override { return true; } +}; + +void setup_preferences() { + auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) + global_preferences = prefs; +} + +} // namespace rp2040 + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/preferences.h b/esphome/components/rp2040/preferences.h new file mode 100644 index 0000000000..3c740a41c1 --- /dev/null +++ b/esphome/components/rp2040/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_RP2040 + +namespace esphome { +namespace rp2040 { + +void setup_preferences(); + +} // namespace rp2040 +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040_pwm/__init__.py b/esphome/components/rp2040_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py new file mode 100644 index 0000000000..8f2972d4a0 --- /dev/null +++ b/esphome/components/rp2040_pwm/output.py @@ -0,0 +1,55 @@ +from esphome import pins, automation +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["rp2040"] + + +rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") +RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) +SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) +validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(RP2040PWM), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + + +@automation.register_action( + "output.rp2040_pwm.set_frequency", + SetFrequencyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(RP2040PWM), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), + } + ), +) +async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + return var diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp new file mode 100644 index 0000000000..bf2a446edf --- /dev/null +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -0,0 +1,45 @@ +#ifdef USE_RP2040 + +#include "rp2040_pwm.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/macros.h" + +#include + +namespace esphome { +namespace rp2040_pwm { + +static const char *const TAG = "rp2040_pwm"; + +void RP2040PWM::setup() { + ESP_LOGCONFIG(TAG, "Setting up RP2040 PWM Output..."); + this->pin_->setup(); + this->pwm_ = new mbed::PwmOut((PinName) this->pin_->get_pin()); + this->turn_off(); +} +void RP2040PWM::dump_config() { + ESP_LOGCONFIG(TAG, "RP2040 PWM:"); + LOG_PIN(" Pin: ", this->pin_); + ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); + LOG_FLOAT_OUTPUT(this); +} +void HOT RP2040PWM::write_state(float state) { + this->last_output_ = state; + + // Also check pin inversion + if (this->pin_->is_inverted()) { + state = 1.0f - state; + } + + auto total_time_us = static_cast(roundf(1e6f / this->frequency_)); + + this->pwm_->period_us(total_time_us); + this->pwm_->write(state); +} + +} // namespace rp2040_pwm +} // namespace esphome + +#endif diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h new file mode 100644 index 0000000000..e348f131c2 --- /dev/null +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -0,0 +1,57 @@ +#pragma once + +#ifdef USE_RP2040 + +#include "esphome/components/output/float_output.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include "drivers/PwmOut.h" + +namespace esphome { +namespace rp2040_pwm { + +class RP2040PWM : public output::FloatOutput, public Component { + public: + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } + void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically update frequency + void update_frequency(float frequency) override { + this->set_frequency(frequency); + this->write_state(this->last_output_); + } + + /// Initialize pin + void setup() override; + void dump_config() override; + /// HARDWARE setup_priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + + InternalGPIOPin *pin_; + mbed::PwmOut *pwm_; + float frequency_{1000.0}; + /// Cache last output level for dynamic frequency updating + float last_output_{0.0}; +}; + +template class SetFrequencyAction : public Action { + public: + SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->update_frequency(freq); + } + + RP2040PWM *parent_; +}; + +} // namespace rp2040_pwm +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 8e9502be6d..81203fdc31 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -13,6 +13,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_IMPLEMENTATION, esp8266=IMPLEMENTATION_LWIP_TCP, esp32=IMPLEMENTATION_BSD_SOCKETS, + rp2040=IMPLEMENTATION_LWIP_TCP, ): cv.one_of( IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" ), diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index a383c0071d..1e79c8a1ab 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -91,7 +91,7 @@ struct iovec { size_t iov_len; }; -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_RP2040) // arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define #ifdef INADDR_ANY #undef INADDR_ANY diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index c917fe1ad8..eeaf37985b 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -46,9 +46,7 @@ async def to_code(config): mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("SPI", None) - if CORE.is_esp8266: + if CORE.using_arduino: cg.add_library("SPI", None) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 7f0b0f481a..bc183be54b 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -105,7 +105,11 @@ class SPIComponent : public Component { void write_byte(uint8_t data) { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { +#ifdef USE_RP2040 + this->hw_spi_->transfer(data); +#else this->hw_spi_->write(data); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -116,7 +120,11 @@ class SPIComponent : public Component { void write_byte16(const uint16_t data) { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { +#ifdef USE_RP2040 + this->hw_spi_->transfer16(data); +#else this->hw_spi_->write16(data); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -130,7 +138,11 @@ class SPIComponent : public Component { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { for (size_t i = 0; i < length; i++) { +#ifdef USE_RP2040 + this->hw_spi_->transfer16(data[i]); +#else this->hw_spi_->write16(data[i]); +#endif } return; } @@ -145,7 +157,11 @@ class SPIComponent : public Component { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { auto *data_c = const_cast(data); +#ifdef USE_RP2040 + this->hw_spi_->transfer(data_c, length); +#else this->hw_spi_->writeBytes(data_c, length); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -178,7 +194,11 @@ class SPIComponent : public Component { if (this->miso_ != nullptr) { this->hw_spi_->transfer(data, length); } else { +#ifdef USE_RP2040 + this->hw_spi_->transfer(data, length); +#else this->hw_spi_->writeBytes(data, length); +#endif } return; } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 93ea6b92a4..f5684f06f7 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -267,7 +267,7 @@ CONFIG_SCHEMA = cv.All( CONF_REBOOT_TIMEOUT, default="15min" ): cv.positive_time_period_milliseconds, cv.SplitDefault( - CONF_POWER_SAVE_MODE, esp8266="none", esp32="light" + CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", rp2040="light" ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, @@ -386,6 +386,8 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFi", None) + elif CORE.is_rp2040: + cg.add_library("WiFi", None) if CORE.is_esp32 and CORE.using_esp_idf: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0103106222..1ed9fd060f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -710,6 +710,8 @@ int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } +bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; } + WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace wifi diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6c5202ed7a..dba0d48724 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -24,6 +24,16 @@ extern "C" { #endif #endif +#ifdef USE_RP2040 +extern "C" { +#include "cyw43.h" +#include "cyw43_country.h" +#include "pico/cyw43_arch.h" +} + +#include +#endif + namespace esphome { namespace wifi { @@ -138,6 +148,8 @@ class WiFiScanResult { float get_priority() const { return priority_; } void set_priority(float priority) { priority_ = priority; } + bool operator==(const WiFiScanResult &rhs) const; + protected: bool matches_{false}; bssid_t bssid_; @@ -310,6 +322,11 @@ class WiFiComponent : public Component { void wifi_process_event_(IDFWiFiEvent *data); #endif +#ifdef USE_RP2040 + static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); + void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); +#endif + std::string use_address_; std::vector sta_; std::vector sta_priorities_; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp new file mode 100644 index 0000000000..0ab143a9de --- /dev/null +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -0,0 +1,204 @@ + +#include "wifi_component.h" + +#ifdef USE_RP2040 + +#include "lwip/dns.h" +#include "lwip/err.h" +#include "lwip/netif.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace wifi { + +static const char *const TAG = "wifi_pico_w"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + if (sta.has_value()) { + if (sta.value()) { + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE); + } + } + return true; +} + +bool WiFiComponent::wifi_apply_power_save_() { + uint32_t pm; + switch (this->power_save_) { + case WIFI_POWER_SAVE_NONE: + pm = CYW43_PERFORMANCE_PM; + break; + case WIFI_POWER_SAVE_LIGHT: + pm = CYW43_DEFAULT_PM; + break; + case WIFI_POWER_SAVE_HIGH: + pm = CYW43_AGGRESSIVE_PM; + break; + } + int ret = cyw43_wifi_pm(&cyw43_state, pm); + return ret == 0; +} + +// TODO: The driver doesnt seem to have an API for this +bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } + +bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) + return false; + + auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); + if (ret != WL_CONNECTED) + return false; + + return true; +} + +bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } + +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + if (!manual_ip.has_value()) { + return true; + } + + IPAddress ip_address = IPAddress(manual_ip->static_ip); + IPAddress gateway = IPAddress(manual_ip->gateway); + IPAddress subnet = IPAddress(manual_ip->subnet); + + IPAddress dns = IPAddress(manual_ip->dns1); + + WiFi.config(ip_address, dns, gateway, subnet); + return true; +} + +bool WiFiComponent::wifi_apply_hostname_() { + WiFi.setHostname(App.get_name().c_str()); + return true; +} +const char *get_auth_mode_str(uint8_t mode) { + // TODO: + return "UNKNOWN"; +} +const char *get_disconnect_reason_str(uint8_t reason) { + // TODO: + return "UNKNOWN"; +} + +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { + int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + switch (status) { + case CYW43_LINK_JOIN: + case CYW43_LINK_NOIP: + return WiFiSTAConnectStatus::CONNECTING; + case CYW43_LINK_UP: + return WiFiSTAConnectStatus::CONNECTED; + case CYW43_LINK_FAIL: + case CYW43_LINK_BADAUTH: + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + case CYW43_LINK_NONET: + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + } + return WiFiSTAConnectStatus::IDLE; +} + +int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + global_wifi_component->wifi_scan_result(env, result); + return 0; +} + +void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + bssid_t bssid; + std::copy(result->bssid, result->bssid + 6, bssid.begin()); + std::string ssid(reinterpret_cast(result->ssid)); + WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty()); + if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { + this->scan_result_.push_back(res); + } +} + +bool WiFiComponent::wifi_scan_start_() { + this->scan_result_.clear(); + this->scan_done_ = false; + cyw43_wifi_scan_options_t scan_options = {0}; + int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); + if (err) { + ESP_LOGV(TAG, "cyw43_wifi_scan failed!"); + } + return err == 0; + return true; +} + +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + // TODO: + return false; +} + +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + if (!this->wifi_mode_({}, true)) + return false; + + if (ap.get_channel().has_value()) { + cyw43_wifi_ap_set_channel(&cyw43_state, ap.get_channel().value()); + } + + const char *ssid = ap.get_ssid().c_str(); + + cyw43_wifi_ap_set_ssid(&cyw43_state, strlen(ssid), (const uint8_t *) ssid); + + if (!ap.get_password().empty()) { + const char *password = ap.get_password().c_str(); + cyw43_wifi_ap_set_password(&cyw43_state, strlen(password), (const uint8_t *) password); + cyw43_wifi_ap_set_auth(&cyw43_state, CYW43_AUTH_WPA2_MIXED_PSK); + } else { + cyw43_wifi_ap_set_auth(&cyw43_state, CYW43_AUTH_OPEN); + } + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE); + + return true; +} +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.localIP()}; } + +bool WiFiComponent::wifi_disconnect_() { + int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); + return err == 0; +} +// NOTE: The driver does not provide an interface to get this +bssid_t WiFiComponent::wifi_bssid() { + bssid_t bssid{}; + uint8_t raw_bssid[6]; + WiFi.BSSID(raw_bssid); + for (size_t i = 0; i < bssid.size(); i++) + bssid[i] = raw_bssid[i]; + return bssid; +} +// NOTE: The driver does not provide an interface to get this +std::string WiFiComponent::wifi_ssid() { return WiFi.SSID(); } +// NOTE: The driver does not provide an interface to get this +int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +// NOTE: The driver does not provide an interface to get this +int32_t WiFiComponent::wifi_channel_() { return 0; } +network::IPAddress WiFiComponent::wifi_sta_ip() { return {WiFi.localIP()}; } +network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } +network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } +network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { + const ip_addr_t *dns_ip = dns_getserver(num); + return {dns_ip->addr}; +} + +void WiFiComponent::wifi_loop_() { + if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { + this->scan_done_ = true; + ESP_LOGV(TAG, "Scan done!"); + } +} + +void WiFiComponent::wifi_pre_setup_() {} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 09436c1fbf..19d4747575 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1440,6 +1440,7 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + rp2040=vol.UNDEFINED, ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1449,6 +1450,7 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._rp2040_default = vol.default_factory(rp2040) @property def default(self): @@ -1458,6 +1460,8 @@ class SplitDefault(Optional): return self._esp32_arduino_default if CORE.is_esp32 and CORE.using_esp_idf: return self._esp32_idf_default + if CORE.is_rp2040: + return self._rp2040_default raise NotImplementedError @default.setter diff --git a/esphome/const.py b/esphome/const.py index f2d31bd21f..f8551f81ce 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -6,8 +6,9 @@ ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" +PLATFORM_RP2040 = "rp2040" -TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266] +TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a422cd9507..ef44b31fa3 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -594,6 +594,10 @@ class EsphomeCore: def is_esp32(self): return self.target_platform == "esp32" + @property + def is_rp2040(self): + return self.target_platform == "rp2040" + @property def target_framework(self): return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK] diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 3aca944a36..79e706cf0d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -21,6 +21,8 @@ #include "esp_system.h" #include #include +#elif defined(USE_RP2040) && defined(USE_WIFI) +#include #endif #ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC @@ -91,6 +93,8 @@ uint32_t random_uint32() { return esp_random(); #elif defined(USE_ESP8266) return os_random(); +#elif defined(USE_RP2040) + return ((uint32_t) rand()) << 16 + ((uint32_t) rand()); #else #error "No random source available for this configuration." #endif @@ -102,6 +106,8 @@ bool random_bytes(uint8_t *data, size_t len) { return true; #elif defined(USE_ESP8266) return os_get_random(data, len) == 0; +#elif defined(USE_RP2040) + return false; #else #error "No random source available for this configuration." #endif @@ -409,6 +415,8 @@ void get_mac_address_raw(uint8_t *mac) { #endif #elif defined(USE_ESP8266) wifi_get_macaddr(STATION_IF, mac); +#elif defined(USE_RP2040) && defined(USE_WIFI) + WiFi.macAddress(mac); #endif } std::string get_mac_address() { diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0367101023..d551c9da4b 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -428,21 +428,27 @@ class DownloadBinaryRequestHandler(BaseHandler): def get(self, configuration=None): type = self.get_argument("type", "firmware.bin") - if type == "firmware.bin": - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return + storage_path = ext_storage_path(settings.config_dir, configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + if storage_json.target_platform.lower() == const.PLATFORM_RP2040: + filename = f"{storage_json.name}.uf2" + path = storage_json.firmware_bin_path.replace( + "firmware.bin", "firmware.uf2" + ) + + elif storage_json.target_platform.lower() == const.PLATFORM_ESP8266: + filename = f"{storage_json.name}.bin" + path = storage_json.firmware_bin_path + + elif type == "firmware.bin": filename = f"{storage_json.name}.bin" path = storage_json.firmware_bin_path elif type == "firmware-factory.bin": - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return filename = f"{storage_json.name}-factory.bin" path = storage_json.firmware_bin_path.replace( "firmware.bin", "firmware-factory.bin" diff --git a/esphome/storage_json.py b/esphome/storage_json.py index af71d4583c..1cc2180747 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -119,16 +119,16 @@ class StorageJSON: ) @staticmethod - def from_wizard(name: str, address: str, esp_platform: str) -> "StorageJSON": + def from_wizard(name: str, address: str, platform: str) -> "StorageJSON": return StorageJSON( storage_version=1, name=name, comment=None, - esphome_version=const.__version__, + esphome_version=None, src_version=1, address=address, web_port=None, - target_platform=esp_platform, + target_platform=platform, build_path=None, firmware_bin_path=None, loaded_integrations=[], diff --git a/esphome/wizard.py b/esphome/wizard.py index 602f4ecf04..6273eec25d 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -81,11 +81,20 @@ esp32: type: esp-idf """ +RP2040_CONFIG = """ +rp2040: + board: {board} + framework: + # Required until https://github.com/platformio/platform-raspberrypi/pull/36 is merged + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git +""" + HARDWARE_BASE_CONFIGS = { "ESP8266": ESP8266_CONFIG, "ESP32": ESP32_CONFIG, "ESP32S2": ESP32S2_CONFIG, "ESP32C3": ESP32C3_CONFIG, + "RP2040": RP2040_CONFIG, } @@ -164,6 +173,7 @@ captive_portal: def wizard_write(path, **kwargs): from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.rp2040 import boards as rp2040_boards name = kwargs["name"] board = kwargs["board"] @@ -173,9 +183,13 @@ def wizard_write(path, **kwargs): kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: - kwargs["platform"] = ( - "ESP8266" if board in esp8266_boards.ESP8266_BOARD_PINS else "ESP32" - ) + if board in esp8266_boards.ESP8266_BOARD_PINS: + platform = "ESP8266" + elif board in rp2040_boards.RP2040_BOARD_PINS: + platform = "RP2040" + else: + platform = "ESP32" + kwargs["platform"] = platform hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) diff --git a/platformio.ini b/platformio.ini index 9b943242f7..86617586ae 100644 --- a/platformio.ini +++ b/platformio.ini @@ -146,6 +146,24 @@ build_flags = -DUSE_ESP32_FRAMEWORK_ESP_IDF extra_scripts = post:esphome/components/esp32/post_build.py.script +; These are common settings for the RP2040 using Arduino. +[common:rp2040-arduino] +extends = common:arduino +board_build.core = earlephilhower +board_build.filesystem_size = 0.5m + +platform = https://github.com/maxgerhardt/platform-raspberrypi.git +platform_packages = + earlephilhower/framework-arduinopico @ ~1.20400.0 + +framework = arduino +lib_deps = + ${common:arduino.lib_deps} +build_flags = + ${common:arduino.build_flags} + -DUSE_RP2040 + -DUSE_RP2040_FRAMEWORK_ARDUINO + ; All the actual environments are defined below. [env:esp8266-arduino] extends = common:esp8266-arduino @@ -222,3 +240,10 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} + +[env:rp2040-pico-arduino] +extends = common:rp2040-arduino +board = pico +build_flags = + ${common:rp2040-arduino.build_flags} + ${flags:runtime.build_flags} diff --git a/script/ci-custom.py b/script/ci-custom.py index 6f69b55d2c..f95039576b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -534,6 +534,7 @@ def lint_relative_py_import(fname): "esphome/components/socket/headers.h", "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", + "esphome/components/rp2040/core.cpp", ], ) def lint_namespace(fname, content): diff --git a/tests/README.md b/tests/README.md index 546025526f..ed78b3e7d1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,3 +24,4 @@ Current test_.yaml file contents. | test3.yaml | ESP8266 | wifi | N/A | test4.yaml | ESP32 | ethernet | None | test5.yaml | ESP32 | wifi | ble_server +| test6.yaml | RP2040 | wifi | N/A diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index d956387665..8cc1838d94 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,8 +13,9 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); - auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); + log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); auto *wifi = new wifi::WiFiComponent(); // NOLINT diff --git a/tests/test6.yaml b/tests/test6.yaml new file mode 100644 index 0000000000..264773331e --- /dev/null +++ b/tests/test6.yaml @@ -0,0 +1,39 @@ +--- +esphome: + name: test6 + project: + name: esphome.test6_project + version: "1.0.0" + +rp2040: + board: rpipicow + framework: + # Waiting for https://github.com/platformio/platform-raspberrypi/pull/36 + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git + + +wifi: + networks: + - ssid: "MySSID" + password: "password1" + +api: + +ota: + +logger: + +binary_sensor: + - platform: gpio + pin: GPIO5 + id: pin_5_button + +output: + - platform: gpio + pin: GPIO4 + id: pin_4 + +switch: + - platform: output + output: pin_4 + id: pin_4_switch From 615288c151ae83c364b1ade26de0c31c8e2efd64 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Oct 2022 01:59:41 +1300 Subject: [PATCH 052/106] Bump esphome-dashboard to 20221020.0 (#3920) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index af7ac0cab4..3b6d444c22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.1.4 # When updating platformio, also update Dockerfile esptool==3.3.1 click==8.1.3 -esphome-dashboard==20221007.0 +esphome-dashboard==20221020.0 aioesphomeapi==10.13.0 zeroconf==0.39.1 From 96e8cb66b69be4233dd6b1d9ae2498ba6888c683 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:12:55 +1300 Subject: [PATCH 053/106] Fix missing dependencies for heatpumpir (#3933) --- esphome/components/heatpumpir/climate.py | 4 ++++ platformio.ini | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 2e78db8356..cc8e75dcbd 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_VISUAL, ) +from esphome.core import CORE CODEOWNERS = ["@rob-deutsch"] @@ -115,3 +116,6 @@ def to_code(config): cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) cg.add_library("tonia/HeatpumpIR", "1.0.20") + + if CORE.is_esp8266 or CORE.is_esp32: + cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12") diff --git a/platformio.ini b/platformio.ini index 86617586ae..ccfee97207 100644 --- a/platformio.ini +++ b/platformio.ini @@ -92,6 +92,7 @@ lib_deps = ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) + crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir build_flags = ${common:arduino.build_flags} -Wno-nonnull-compare @@ -111,16 +112,17 @@ board = nodemcu-32s lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first - FS ; web_server_base (Arduino built-in) - WiFi ; wifi,web_server_base,ethernet (Arduino built-in) - Update ; ota,web_server_base (Arduino built-in) + FS ; web_server_base (Arduino built-in) + WiFi ; wifi,web_server_base,ethernet (Arduino built-in) + Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@1.2.2 ; async_tcp - WiFiClientSecure ; http_request,nextion (Arduino built-in) - HTTPClient ; http_request,nextion (Arduino built-in) - ESPmDNS ; mdns (Arduino built-in) - DNSServer ; captive_portal (Arduino built-in) - esphome/ESP32-audioI2S@2.1.0 ; i2s_audio + esphome/AsyncTCP-esphome@1.2.2 ; async_tcp + WiFiClientSecure ; http_request,nextion (Arduino built-in) + HTTPClient ; http_request,nextion (Arduino built-in) + ESPmDNS ; mdns (Arduino built-in) + DNSServer ; captive_portal (Arduino built-in) + esphome/ESP32-audioI2S@2.1.0 ; i2s_audio + crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir build_flags = ${common:arduino.build_flags} -DUSE_ESP32 From 3a134ef009357235da786fa448dad1c930f0ef2c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:53:01 +1300 Subject: [PATCH 054/106] Update the PR template (#3934) --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f9c8cce0af..3221b8ac5c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ # What does this implement/fix? -Quick description and explanation of changes + ## Types of changes @@ -18,6 +18,7 @@ Quick description and explanation of changes - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 +- [ ] RP2040 ## Example entry for `config.yaml`: