Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions components/ble_elm327/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
30 changes: 29 additions & 1 deletion components/ble_elm327/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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

23 changes: 23 additions & 0 deletions components/ble_elm327/automation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include "esphome/core/automation.h"
#include "ble_elm327.h"

namespace esphome {
namespace ble_elm327 {

template<typename... Ts> class BleElm327SendCommandAction : public Action<Ts...> {
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
13 changes: 11 additions & 2 deletions components/ble_elm327/ble_elm327.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
Expand Down Expand Up @@ -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; }
Expand Down
7 changes: 6 additions & 1 deletion components/ble_elm327/ble_elm327.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

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

Expand Down Expand Up @@ -141,4 +144,6 @@ class BleElm327Component : public Component, public ble_client::BLEClientNode {

} // namespace ble_elm327
} // namespace esphome

#include "automation.h"
#endif
1 change: 1 addition & 0 deletions components/ble_elm327/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
CONF_MODE = "mode"
CONF_FORMULA = "formula"
CONF_PRESET = "preset"
CONF_COMMAND = "command"