1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-13 22:28:14 +00:00

Add component to query car data over OBD can bus

This commit is contained in:
Marcel Koonstra 2024-12-22 06:54:00 +01:00
parent c457d8835e
commit 837fec7f72
5 changed files with 550 additions and 0 deletions

View File

@ -0,0 +1,93 @@
import esphome.config_validation as cv
import esphome.codegen as cg
import esphome.components.sensor as s
import esphome.components.binary_sensor as bs
from esphome import automation
from esphome.const import (
CONF_ID,
CONF_INTERVAL,
CONF_TRIGGER_ID
)
obd_ns = cg.esphome_ns.namespace("obd")
OBDComponent = obd_ns.class_("OBDComponent", cg.PollingComponent)
OBDSensor = obd_ns.class_("OBDSensor", s.Sensor, cg.Component)
OBDBinarySensor = obd_ns.class_("OBDBinarySensor", bs.BinarySensor, cg.Component)
PIDRequest = obd_ns.class_("PIDRequest", cg.Component)
OBDPidTrigger = obd_ns.class_(
"OBDPidTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8)),
cg.Component
)
CONF_CANBUS_ID = 'canbus_id'
CONF_ENABLED_BY_DEFAULT = 'enabled_by_default'
CONF_OBD_ID = 'obd_id'
CONF_PID_ID = "pid_id"
CONF_CAN_ID = "can_id"
CONF_RESPONSE_CAN_ID = "response_can_id"
CONF_USE_EXTENDED_ID = "use_extended_id"
CONF_PIDS = "pids"
CONF_PID = "pid"
CONF_TIMEOUT = "timeout"
CONF_REPLY_LENGTH = "reply_length"
CONF_ON_FRAME = "on_frame"
CONF_MASK = "mask"
CONF_SIGNED = "signed"
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(OBDComponent),
cv.Required(CONF_CANBUS_ID): cv.use_id("CanbusComponent"),
cv.Optional(CONF_ENABLED_BY_DEFAULT, default=False): cv.boolean,
cv.Optional(CONF_PIDS): cv.ensure_list(
{
cv.GenerateID(CONF_ID): cv.declare_id(PIDRequest),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Required(CONF_PID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_RESPONSE_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_INTERVAL, default="5s"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_TIMEOUT, default="500ms"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_REPLY_LENGTH, default=8): cv.positive_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OBDPidTrigger),
}
),
}
)
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
can_bus = await cg.get_variable(config[CONF_CANBUS_ID])
cg.add(var.set_canbus(can_bus))
cg.add(var.set_enabled_by_default(config[CONF_ENABLED_BY_DEFAULT]))
for pid_conf in config.get(CONF_PIDS, []):
can_id = pid_conf[CONF_CAN_ID]
pid = pid_conf[CONF_PID]
use_extended_id = pid_conf[CONF_USE_EXTENDED_ID]
response_can_id = pid_conf.get(CONF_RESPONSE_CAN_ID)
if (response_can_id is None):
response_can_id = can_id | 8
pid_request = cg.new_Pvariable(
pid_conf[CONF_ID], var, can_id, pid, response_can_id, use_extended_id
)
await cg.register_component(pid_request, pid_conf)
cg.add(pid_request.set_interval(pid_conf[CONF_INTERVAL]))
cg.add(pid_request.set_timeout(pid_conf[CONF_TIMEOUT]))
cg.add(pid_request.set_reply_length(pid_conf[CONF_REPLY_LENGTH]))
for trigger_conf in pid_conf.get(CONF_ON_FRAME, []):
trigger = cg.new_Pvariable(trigger_conf[CONF_TRIGGER_ID], pid_request)
await cg.register_component(trigger, trigger_conf)
await automation.build_automation(trigger, [(cg.std_vector.template(cg.uint8), "data")], trigger_conf)
return var

View File

@ -0,0 +1,49 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.cpp_generator import LambdaExpression
from esphome.const import (
CONF_LAMBDA,
CONF_INDEX,
)
from . import (
CONF_PID_ID,
CONF_MASK,
OBDBinarySensor,
PIDRequest
)
CONFIG_SCHEMA = cv.All(
binary_sensor.binary_sensor_schema(
OBDBinarySensor,
)
.extend(
{
cv.Required(CONF_PID_ID): cv.use_id(PIDRequest),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_INDEX): cv.positive_int,
cv.Optional(CONF_MASK): cv.hex_int,
}
),
cv.has_exactly_one_key(CONF_LAMBDA, CONF_INDEX),
cv.has_none_or_all_keys(CONF_INDEX, CONF_MASK)
)
async def to_code(config):
pid_request = await cg.get_variable(config[CONF_PID_ID])
var = await binary_sensor.new_binary_sensor(config, pid_request)
await cg.register_component(var, config)
if lambda_ := config.get(CONF_LAMBDA):
template = await cg.process_lambda(lambda_, [(cg.std_vector.template(cg.uint8), "data")], return_type=cg.bool_)
cg.add(var.set_template(template))
else:
index = config[CONF_INDEX]
mask = config[CONF_MASK]
template = LambdaExpression(
f"return (data[{index}] & {mask}) == {mask};",
[(cg.std_vector.template(cg.uint8), "data")],
return_type=cg.bool_
)
cg.add(var.set_template(template))

View File

@ -0,0 +1,179 @@
#include "obd_component.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/components/canbus/canbus.h"
namespace esphome {
namespace obd {
static const char *const TAG = "obd";
// Skip setup from PollingComponent to stop polling from starting automatically
void OBDComponent::call_setup() {
if (this->enabled_by_default_) {
this->start_poller();
}
}
void OBDComponent::dump_config() {
ESP_LOGCONFIG(TAG, "OBD Component");
}
void OBDComponent::update() {
if (this->current_request_ == nullptr) {
// No current request, find the next request to do
for (auto *request : this->pidrequests_) {
if (request->start()) {
this->current_request_ = request;
return;
}
}
// No pid to be polled, exit loop
return;
} else {
if (this->current_request_->update()) {
this->current_request_ = nullptr;
}
}
}
void OBDComponent::send(std::uint32_t can_id, bool use_extended_id, uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
std::vector<uint8_t> data(8, 0xAA);
data[0] = a;
data[1] = b;
data[2] = c;
data[3] = d;
this->canbus_->send_data(can_id, use_extended_id, data);
}
void OBDComponent::send(std::uint32_t can_id, bool use_extended_id, uint8_t a, uint8_t b, uint8_t c) {
std::vector<uint8_t> data(8, 0xAA);
data[0] = a;
data[1] = b;
data[2] = c;
this->canbus_->send_data(can_id, use_extended_id, data);
}
void OBDComponent::add_pidrequest(PIDRequest *request) {
ESP_LOGVV(TAG, "add request for canid=0x%03, pid=0x%06" PRIx32, request->can_id_, PRIx32, request->pid_);
this->pidrequests_.push_back(request);
};
void PIDRequest::setup() {
this->parent_->add_pidrequest(this);
auto trigger = new OBDCanbusTrigger(this);
trigger->setup();
}
bool PIDRequest::start() {
if (this->state_ != WAITING)
return false;
if ((this->last_polled_ + this->interval_) >= millis())
return false;
this->response_buffer_.clear();
this->response_buffer_.reserve(this->reply_length_);
auto can_id = this->can_id_;
auto pid = this->pid_;
ESP_LOGD(TAG, "polling can_id: 0x%03x for pid 0x%04x", can_id, pid);
if (pid > 0xFFFF) {
// 24 bit pid
this->parent_->send(can_id, this->use_extended_id_, 0x03, (pid >> 16) & 0xFF, (pid >> 8) & 0xFF, pid & 0xFF);
} else {
this->parent_->send(can_id, this->use_extended_id_, 0x02, (pid >> 8) & 0xFF, pid & 0xFF);
}
this->last_polled_ = millis();
this->state_ = POLLING;
return true;
}
bool PIDRequest::update() {
if (this->state_ != POLLING)
return true; // Invalid state, update should not have been called here
if ((this->last_polled_ + this->timeout_) > millis()) {
return false;
}
if (this->response_buffer_.size() < this->reply_length_) {
ESP_LOGD(TAG, "timeout for polling can_id: 0x%03x for pid 0x%04x", this->can_id_, this->pid_);
this->state_ = WAITING;
return true;
}
for (auto *trigger : this->triggers_) {
trigger->trigger(this->response_buffer_);
}
for (auto *sensor : this->sensors_) {
sensor->update(this->response_buffer_);
}
this->state_ = WAITING;
return true;
}
void PIDRequest::handle_incoming(std::vector<uint8_t> &data) {
if (this->state_ != POLLING)
return; // Not our cup of tea here, some other pid might be polling on the same can_id
ESP_LOGD(TAG, "recieved content for pid 0x%04x: %s", this->can_id_, this->pid_, format_hex_pretty(data).c_str());
// Handle the data
if ((data[0] & 0xF0) == 0x10) {
// This is a flow control frame, we should ask for more
this->parent_->send(this->can_id_, this->use_extended_id_, 0x30, 0x0, 0x10);
}
for (int i = 0; i < data.size(); i++) {
this->response_buffer_.push_back(data[i]);
}
}
void PIDRequest::dump_config() {
ESP_LOGCONFIG(TAG, "PID Request 0x%03", this->pid_);
if (this->use_extended_id_) {
ESP_LOGCONFIG(TAG, " Can extended id: 0x%08" PRIx32, this->can_id_);
ESP_LOGCONFIG(TAG, " Can response id: 0x%08" PRIx32, this->can_response_id_);
} else {
ESP_LOGCONFIG(TAG, " Can id: 0x%03" PRIx32, this->can_id_);
ESP_LOGCONFIG(TAG, " Can response id: 0x%03" PRIx32, this->can_response_id_);
}
ESP_LOGCONFIG(TAG, " Pid: 0x%03" PRIx32, this->pid_);
ESP_LOGCONFIG(TAG, " Interval: %ims", this->interval_);
ESP_LOGCONFIG(TAG, " Timeout: %ims", this->timeout_);
}
void OBDSensor::update(const std::vector<uint8_t> &data) {
auto value = this->data_to_value_func_(data);
this->publish_state(value);
}
void OBDSensor::dump_config() {
LOG_SENSOR("", "OBD Sensor", this);
}
void OBDBinarySensor::update(const std::vector<uint8_t> &data) {
auto value = this->data_to_value_func_(data);
this->publish_state(value);
}
void OBDBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "OBD Binary Sensor", this);
}
}
}

View File

@ -0,0 +1,154 @@
#include "esphome/core/component.h"
#include "esphome/components/canbus/canbus.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace obd {
class PIDRequest;
class OBDPidTrigger;
class OBDSensorBase;
class OBDSensor;
class OBDComponent : public PollingComponent {
friend class OBDCanbusTrigger;
public:
explicit OBDComponent() : PollingComponent(500) {}
void call_setup() override;
void update() override;
void set_canbus(canbus::Canbus *canbus) { this->canbus_ = canbus; }
void set_enabled_by_default(bool enabled_by_default) { this->enabled_by_default_ = enabled_by_default; }
void add_pidrequest(PIDRequest *request);
void dump_config() override;
void send(std::uint32_t can_id, bool use_extended_id, uint8_t a, uint8_t b, uint8_t c);
void send(std::uint32_t can_id, bool use_extended_id, uint8_t a, uint8_t b, uint8_t c, uint8_t d);
protected:
canbus::Canbus *canbus_{nullptr};
bool enabled_by_default_{false};
std::vector<PIDRequest *> pidrequests_{};
PIDRequest* current_request_{nullptr};
};
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
using data_to_bool_t = std::function<float(std::vector<uint8_t>)>;
enum request_state_t {
WAITING,
POLLING,
};
class PIDRequest : public Component {
friend class OBDCanbusTrigger;
public:
explicit PIDRequest(OBDComponent *parent, const std::uint32_t can_id, const std::uint32_t pid,
const std::uint32_t can_response_id, const bool use_extended_id)
: parent_(parent), can_id_(can_id), pid_(pid), can_response_id_(can_response_id), use_extended_id_(use_extended_id){
this->state_ = WAITING;
};
void setup() override;
uint32_t last_polled_{0};
bool start();
bool update();
void set_timeout(std::uint32_t timeout) { this->timeout_ = timeout; }
void set_interval(std::uint32_t interval) { this->interval_ = interval; }
void set_reply_length(std::uint32_t reply_length) { this->reply_length_ = reply_length; }
void add_sensor(OBDSensorBase *sensor) { this->sensors_.push_back(sensor); }
void add_trigger(OBDPidTrigger *trigger) { this->triggers_.push_back(trigger); }
void dump_config() override;
protected:
void handle_incoming(std::vector<uint8_t> &data);
OBDComponent *parent_;
uint32_t can_id_;
uint32_t can_response_id_;
uint32_t pid_;
bool use_extended_id_;
uint32_t interval_{5000};
uint32_t timeout_{500};
uint32_t reply_length_{8};
request_state_t state_{WAITING};
std::vector<uint8_t> response_buffer_{};
std::vector<OBDSensorBase *> sensors_{};
std::vector<OBDPidTrigger *> triggers_{};
};
class OBDPidTrigger : public Trigger<std::vector<uint8_t>>, public Component {
public:
explicit OBDPidTrigger(PIDRequest *parent) : parent_(parent){}
void setup() override { this->parent_->add_trigger(this); }
protected:
PIDRequest *parent_;
};
class OBDSensorBase {
public:
virtual void update(const std::vector<uint8_t> &data) {}
};
class OBDSensor : public OBDSensorBase, public sensor::Sensor, public Component {
public:
explicit OBDSensor(PIDRequest *parent) : parent_(parent){}
void set_template(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
void update(const std::vector<uint8_t> &data) override ;
void setup() override { this->parent_->add_sensor(this); }
void dump_config() override;
protected:
PIDRequest *parent_;
data_to_value_t data_to_value_func_{};
};
class OBDBinarySensor : public OBDSensorBase, public binary_sensor::BinarySensor, public Component {
public:
explicit OBDBinarySensor(PIDRequest *parent) : parent_(parent){}
void set_template(data_to_bool_t &&lambda) { this->data_to_value_func_ = lambda; }
void update(const std::vector<uint8_t> &data) override;
void setup() override { this->parent_->add_sensor(this); }
void dump_config() override;
protected:
PIDRequest *parent_;
data_to_bool_t data_to_value_func_;
};
class OBDCanbusTrigger : public canbus::CanbusTrigger, public Action<std::vector<uint8_t>, uint32_t, bool> {
public:
explicit OBDCanbusTrigger(PIDRequest *parent) : CanbusTrigger(parent->parent_->canbus_, parent->can_response_id_, 0x1FFFFFFF, parent->use_extended_id_), parent_(parent){
auto automation = new Automation<std::vector<uint8_t>, uint32_t, bool>(this);
automation->add_action(this);
};
void play(std::vector<uint8_t> data, uint32_t can_id, bool rx) override {
this->parent_->handle_incoming(data);
}
protected:
PIDRequest *parent_;
};
}
}

View File

@ -0,0 +1,75 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import sensor
from esphome.cpp_generator import LambdaExpression
from esphome.const import (
CONF_ID,
CONF_LAMBDA,
CONF_INDEX,
)
from . import (
CONF_PID_ID,
CONF_SIGNED,
OBDSensor,
PIDRequest
)
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
OBDSensor,
)
.extend(
{
cv.Required(CONF_PID_ID): cv.use_id(PIDRequest),
cv.Optional(CONF_SIGNED, default=False): cv.boolean,
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.returning_lambda,
cv.Exclusive(CONF_INDEX, CONF_LAMBDA): cv.ensure_list(cv.positive_int)
}
),
cv.has_exactly_one_key(CONF_LAMBDA, CONF_INDEX)
)
async def to_code(config):
pid_request = await cg.get_variable(config[CONF_PID_ID])
var = await sensor.new_sensor(config, pid_request)
await cg.register_component(var, config)
template = False
if lambda_ := config.get(CONF_LAMBDA):
template = await cg.process_lambda(lambda_, [(cg.std_vector.template(cg.uint8), "data")], return_type=cg.float_)
cg.add(var.set_template(template))
else:
indexes = config[CONF_INDEX]
signed = "(int8_t)" if config[CONF_SIGNED] else ""
if len(indexes) == 4:
template = LambdaExpression(
f"return ({signed}data[{indexes[0]}] << 24) | (data[{indexes[1]}] << 16) | (data[{indexes[2]}] << 8) | data[{indexes[3]}];",
[(cg.std_vector.template(cg.uint8), "data")],
return_type=cg.float_
)
if len(indexes) == 3:
template = LambdaExpression(
f"return ({signed}data[{indexes[0]}] << 16) | (data[{indexes[1]}] << 8) | data[{indexes[2]}];",
[(cg.std_vector.template(cg.uint8), "data")],
return_type=cg.float_
)
if len(indexes) == 2:
template = LambdaExpression(
f"return ({signed}data[{indexes[0]}] << 8) | data[{indexes[1]}];",
[(cg.std_vector.template(cg.uint8), "data")],
return_type=cg.float_
)
if len(indexes) == 1:
template = LambdaExpression(
f"return {signed}data[{indexes[0]}];",
[(cg.std_vector.template(cg.uint8), "data")],
return_type=cg.float_
)
cg.add(var.set_template(template))