diff --git a/esphome/components/obd/__init__.py b/esphome/components/obd/__init__.py new file mode 100644 index 0000000000..8e6f28d83c --- /dev/null +++ b/esphome/components/obd/__init__.py @@ -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 diff --git a/esphome/components/obd/binary_sensor.py b/esphome/components/obd/binary_sensor.py new file mode 100644 index 0000000000..d4fdf53d22 --- /dev/null +++ b/esphome/components/obd/binary_sensor.py @@ -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)) \ No newline at end of file diff --git a/esphome/components/obd/obd_component.cpp b/esphome/components/obd/obd_component.cpp new file mode 100644 index 0000000000..e36896a708 --- /dev/null +++ b/esphome/components/obd/obd_component.cpp @@ -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 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 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 &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 &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 &data) { + auto value = this->data_to_value_func_(data); + this->publish_state(value); +} + +void OBDBinarySensor::dump_config() { + LOG_BINARY_SENSOR("", "OBD Binary Sensor", this); +} + +} +} \ No newline at end of file diff --git a/esphome/components/obd/obd_component.h b/esphome/components/obd/obd_component.h new file mode 100644 index 0000000000..8cbcaff3b5 --- /dev/null +++ b/esphome/components/obd/obd_component.h @@ -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 pidrequests_{}; + + PIDRequest* current_request_{nullptr}; +}; + +using data_to_value_t = std::function)>; +using data_to_bool_t = std::function)>; + +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 &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 response_buffer_{}; + + std::vector sensors_{}; + std::vector triggers_{}; +}; + +class OBDPidTrigger : public Trigger>, 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 &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 &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 &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, 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, uint32_t, bool>(this); + automation->add_action(this); + }; + + void play(std::vector data, uint32_t can_id, bool rx) override { + this->parent_->handle_incoming(data); + } + + protected: + PIDRequest *parent_; +}; + +} +} + diff --git a/esphome/components/obd/sensor.py b/esphome/components/obd/sensor.py new file mode 100644 index 0000000000..26e53889f1 --- /dev/null +++ b/esphome/components/obd/sensor.py @@ -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)) + \ No newline at end of file