From 14b755587df52b8933f99a729fd1e9b03a3ec432 Mon Sep 17 00:00:00 2001 From: David Gleich Date: Fri, 4 Sep 2026 12:19:32 +0200 Subject: [PATCH] Allow to send raw commands --- components/ble_elm327/README.md | 32 ++++++++++++++++++++++++++++ components/ble_elm327/__init__.py | 30 +++++++++++++++++++++++++- components/ble_elm327/automation.h | 23 ++++++++++++++++++++ components/ble_elm327/ble_elm327.cpp | 13 +++++++++-- components/ble_elm327/ble_elm327.h | 7 +++++- components/ble_elm327/const.py | 1 + 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 components/ble_elm327/automation.h diff --git a/components/ble_elm327/README.md b/components/ble_elm327/README.md index 2481002e..007c526a 100644 --- a/components/ble_elm327/README.md +++ b/components/ble_elm327/README.md @@ -22,6 +22,8 @@ The component registers as a node under ESPHome's standard `ble_client:` compone - [Per-device Pre-commands](#per-device-pre-commands) - [Platforms](#platforms) - [Sensor](#sensor) +- [Actions](#actions) + - [ble_elm327.send_command](#ble_elm327send_command) - [Presets](#presets) - [GM Extended PIDs (Mode 22)](#gm-extended-pids-mode-22) - [Response Parsing](#response-parsing) @@ -254,6 +256,36 @@ All standard ESPHome sensor options (`unit_of_measurement`, `device_class`, `sta --- +## Actions + +### `ble_elm327.send_command` + +Enqueues an arbitrary AT command or OBD PID request to be transmitted over BLE to the ELM327 adapter. Commands are queued and transmitted with the configured `tx_delay` timing. + +This is especially useful for closing the active OBD/CAN protocol session (`AT PC` - Protocol Close) or putting the adapter into low-power mode (`AT LP`) before disconnecting BLE, ensuring vehicle ECUs can enter deep sleep and prevent 12V battery drain. + +```yaml +# Simple syntax +- ble_elm327.send_command: "AT PC" + +# Explicit ID syntax +- ble_elm327.send_command: + id: ble_elm327_vgate_dongle + command: "AT PC" + +# Templated command in lambdas / scripts +- ble_elm327.send_command: !lambda |- + return "AT PC"; +``` + +You can also call `send_command()` directly from C++ lambdas: + +```cpp +id(ble_elm327_vgate_dongle).send_command("AT PC"); +``` + +--- + ## Presets Standard OBD-II Mode 01 PIDs are built in. Use `preset:` instead of specifying `pid`, `mode`, `formula`, `unit_of_measurement`, `device_class`, `state_class`, and `accuracy_decimals` manually. diff --git a/components/ble_elm327/__init__.py b/components/ble_elm327/__init__.py index 785e1316..5c92727a 100644 --- a/components/ble_elm327/__init__.py +++ b/components/ble_elm327/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import ble_client, esp32_ble_tracker from esphome.const import ( - CONF_ID, CONF_SERVICE_UUID, + CONF_ID, CONF_SERVICE_UUID, CONF_COMMAND, ) from .const import ( CONF_BLE_ELM327_ID, CONF_BLE_CLIENT_ID, CONF_RX_CHAR_UUID, CONF_TX_CHAR_UUID, @@ -25,6 +26,10 @@ # Device IS a PollingComponent — each sensor owns its update_interval BleElm327Device = ble_elm327_ns.class_("BleElm327Device", cg.PollingComponent) +BleElm327SendCommandAction = ble_elm327_ns.class_( + "BleElm327SendCommandAction", automation.Action +) + def _add_uuid(var, uuid_val, fn16, fn32, fn128): if len(uuid_val) == len(esp32_ble_tracker.bt_uuid16_format): @@ -138,3 +143,26 @@ async def register_ble_elm327_device(var, config): return_type=cg.float_, ) cg.add(var.set_formula(formula_)) + + +SEND_COMMAND_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(BleElm327Component), + cv.Required(CONF_COMMAND): cv.templatable(cv.string), + }, + key=CONF_COMMAND, +) + + +@automation.register_action( + "ble_elm327.send_command", + BleElm327SendCommandAction, + SEND_COMMAND_ACTION_SCHEMA, +) +async def ble_elm327_send_command_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + templ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string) + cg.add(var.set_command(templ)) + return var + diff --git a/components/ble_elm327/automation.h b/components/ble_elm327/automation.h new file mode 100644 index 00000000..cd27c0f7 --- /dev/null +++ b/components/ble_elm327/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "ble_elm327.h" + +namespace esphome { +namespace ble_elm327 { + +template class BleElm327SendCommandAction : public Action { + public: + explicit BleElm327SendCommandAction(BleElm327Component *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, command) + + void play(const Ts &...x) override { + this->parent_->send_command(this->command_.value(x...)); + } + + protected: + BleElm327Component *parent_; +}; + +} // namespace ble_elm327 +} // namespace esphome diff --git a/components/ble_elm327/ble_elm327.cpp b/components/ble_elm327/ble_elm327.cpp index 330548f7..f2a4061e 100644 --- a/components/ble_elm327/ble_elm327.cpp +++ b/components/ble_elm327/ble_elm327.cpp @@ -70,6 +70,15 @@ void BleElm327Component::add_init_command(const std::string &cmd) { extra_init_commands_.push_back(normalized + "\r"); } +void BleElm327Component::send_command(const std::string &cmd) { + if (cmd.empty()) return; + std::string formatted = cmd; + if (formatted.back() != '\r') { + formatted += '\r'; + } + tx_queue_.push({formatted, nullptr}); +} + // ── BleElm327Component ────────────────────────────────────────────────────── void BleElm327Component::loop() { @@ -90,7 +99,7 @@ void BleElm327Component::loop() { auto item = tx_queue_.front(); tx_queue_.pop(); if (item.dev) item.dev->on_dequeue(); - send_command(item.cmd); + send_command_raw_(item.cmd); last_tx_time_ = millis(); if (elm_state_ == ElmState::CONNECTED && tx_queue_.empty()) { elm_state_ = ElmState::READY; @@ -211,7 +220,7 @@ void BleElm327Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gat } } -bool BleElm327Component::send_command(const std::string &cmd) { +bool BleElm327Component::send_command_raw_(const std::string &cmd) { if (client_state_ != espbt::ClientState::ESTABLISHED || tx_char_handle_ == 0) return false; auto *chr = this->parent_->get_characteristic(service_uuid_, tx_char_uuid_); if (chr == nullptr) { ESP_LOGW(TAG, "TX characteristic missing"); return false; } diff --git a/components/ble_elm327/ble_elm327.h b/components/ble_elm327/ble_elm327.h index 4773012c..bde34832 100644 --- a/components/ble_elm327/ble_elm327.h +++ b/components/ble_elm327/ble_elm327.h @@ -96,6 +96,9 @@ class BleElm327Component : public Component, public ble_client::BLEClientNode { void add_init_command(const std::string &cmd); void set_tx_delay(uint32_t ms) { tx_delay_ms_ = ms; } + // Enqueues a command (AT command, PID, etc.) to be transmitted to the ELM327 adapter. + void send_command(const std::string &cmd); + protected: enum class ElmState { IDLE, CONNECTED, READY }; @@ -104,7 +107,7 @@ class BleElm327Component : public Component, public ble_client::BLEClientNode { BleElm327Device *dev{nullptr}; }; - bool send_command(const std::string &cmd); + bool send_command_raw_(const std::string &cmd); void on_notify(const uint8_t *data, uint16_t length); void process_response(const std::string &response); @@ -141,4 +144,6 @@ class BleElm327Component : public Component, public ble_client::BLEClientNode { } // namespace ble_elm327 } // namespace esphome + +#include "automation.h" #endif diff --git a/components/ble_elm327/const.py b/components/ble_elm327/const.py index 85cdd65e..8af358b5 100644 --- a/components/ble_elm327/const.py +++ b/components/ble_elm327/const.py @@ -9,3 +9,4 @@ CONF_MODE = "mode" CONF_FORMULA = "formula" CONF_PRESET = "preset" +CONF_COMMAND = "command"