diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h new file mode 100644 index 0000000000..19427b9159 --- /dev/null +++ b/esphome/components/ezo/automation.h @@ -0,0 +1,53 @@ +#pragma once +#include + +#include "esphome/core/automation.h" +#include "ezo.h" + +namespace esphome { +namespace ezo { + +class LedTrigger : public Trigger { + public: + explicit LedTrigger(EZOSensor *ezo) { + ezo->add_led_state_callback([this](bool value) { this->trigger(value); }); + } +}; + +class CustomTrigger : public Trigger { + public: + explicit CustomTrigger(EZOSensor *ezo) { + ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class TTrigger : public Trigger { + public: + explicit TTrigger(EZOSensor *ezo) { + ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class CalibrationTrigger : public Trigger { + public: + explicit CalibrationTrigger(EZOSensor *ezo) { + ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class SlopeTrigger : public Trigger { + public: + explicit SlopeTrigger(EZOSensor *ezo) { + ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class DeviceInformationTrigger : public Trigger { + public: + explicit DeviceInformationTrigger(EZOSensor *ezo) { + ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 12f88a0f66..2efdc1b984 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -5,11 +5,11 @@ namespace esphome { namespace ezo { -static const char *const TAG = "ezo.sensor"; +static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", + "EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP", + "EZO_I2C", "EZO_T", "EZO_CUSTOM"}; -static const uint16_t EZO_STATE_WAIT = 1; -static const uint16_t EZO_STATE_SEND_TEMP = 2; -static const uint16_t EZO_STATE_WAIT_TEMP = 4; +static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"}; void EZOSensor::dump_config() { LOG_SENSOR("", "EZO", this); @@ -20,37 +20,75 @@ void EZOSensor::dump_config() { } void EZOSensor::update() { - if (this->state_ & EZO_STATE_WAIT) { - ESP_LOGE(TAG, "update overrun, still waiting for previous response"); + // Check if a read is in there already and if not insert on in the second position + + if (!this->commands_.empty() && this->commands_.front()->command_type != EzoCommandType::EZO_READ && + this->commands_.size() > 1) { + bool found = false; + + for (auto &i : this->commands_) { + if (i->command_type == EzoCommandType::EZO_READ) { + found = true; + break; + } + } + + if (!found) { + std::unique_ptr ezo_command(new EzoCommand); + ezo_command->command = "R"; + ezo_command->command_type = EzoCommandType::EZO_READ; + ezo_command->delay_ms = 900; + + auto it = this->commands_.begin(); + ++it; + this->commands_.insert(it, std::move(ezo_command)); + } + return; } - uint8_t c = 'R'; - this->write(&c, 1); - this->state_ |= EZO_STATE_WAIT; - this->start_time_ = millis(); - this->wait_time_ = 900; + + this->get_state(); } void EZOSensor::loop() { - uint8_t buf[21]; - if (!(this->state_ & EZO_STATE_WAIT)) { - if (this->state_ & EZO_STATE_SEND_TEMP) { - int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_); - this->write(buf, len); - this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP; - this->start_time_ = millis(); - this->wait_time_ = 300; + if (this->commands_.empty()) { + return; + } + + EzoCommand *to_run = this->commands_.front().get(); + + if (!to_run->command_sent) { + const uint8_t *data = reinterpret_cast(to_run->command.c_str()); + ESP_LOGVV(TAG, "Sending command \"%s\"", data); + + this->write(data, to_run->command.length()); + + if (to_run->command_type == EzoCommandType::EZO_SLEEP || + to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data + this->commands_.pop_front(); + if (to_run->command_type == EzoCommandType::EZO_I2C) + this->address_ = this->new_address_; + return; } + + this->start_time_ = millis(); + to_run->command_sent = true; return; } - if (millis() - this->start_time_ < this->wait_time_) + + if (millis() - this->start_time_ < to_run->delay_ms) return; + + uint8_t buf[32]; + buf[0] = 0; - if (!this->read_bytes_raw(buf, 20)) { + + if (!this->read_bytes_raw(buf, 32)) { ESP_LOGE(TAG, "read error"); - this->state_ = 0; + this->commands_.pop_front(); return; } + switch (buf[0]) { case 1: break; @@ -66,28 +104,142 @@ void EZOSensor::loop() { ESP_LOGE(TAG, "device returned an unknown response: %d", buf[0]); break; } - if (this->state_ & EZO_STATE_WAIT_TEMP) { - this->state_ = 0; - return; - } - this->state_ &= ~EZO_STATE_WAIT; - if (buf[0] != 1) - return; - // some sensors return multiple comma-separated values, terminate string after first one - for (size_t i = 1; i < sizeof(buf) - 1; i++) { - if (buf[i] == ',') - buf[i] = '\0'; + ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", buf, EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); + + if ((buf[0] == 1) || (to_run->command_type == EzoCommandType::EZO_CALIBRATION)) { // EZO_CALIBRATION returns 0-3 + // some sensors return multiple comma-separated values, terminate string after first one + for (size_t i = 1; i < sizeof(buf) - 1; i++) { + if (buf[i] == ',') { + buf[i] = '\0'; + break; + } + } + std::string payload = reinterpret_cast(&buf[1]); + if (!payload.empty()) { + switch (to_run->command_type) { + case EzoCommandType::EZO_READ: { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + } else { + this->publish_state(*val); + } + break; + } + case EzoCommandType::EZO_LED: { + this->led_callback_.call(payload.back() == '1'); + break; + } + case EzoCommandType::EZO_DEVICE_INFORMATION: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->device_infomation_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_SLOPE: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->slope_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_CALIBRATION: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->calibration_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_T: { + this->t_callback_.call(payload); + break; + } + case EzoCommandType::EZO_CUSTOM: { + this->custom_callback_.call(payload); + break; + } + default: { + break; + } + } + } } - float val = parse_number((char *) &buf[1]).value_or(0); - this->publish_state(val); + this->commands_.pop_front(); } -void EZOSensor::set_tempcomp_value(float temp) { - this->tempcomp_ = temp; - this->state_ |= EZO_STATE_SEND_TEMP; +void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) { + std::unique_ptr ezo_command(new EzoCommand); + ezo_command->command = command; + ezo_command->command_type = command_type; + ezo_command->delay_ms = delay_ms; + this->commands_.push_back(std::move(ezo_command)); +}; + +void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) { + std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value); + this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); } +void EZOSensor::set_address(uint8_t address) { + if (address > 0 && address < 128) { + std::string payload = str_sprintf("I2C,%u", address); + this->new_address_ = address; + this->add_command_(payload, EzoCommandType::EZO_I2C); + } else { + ESP_LOGE(TAG, "Invalid I2C address"); + } +} + +void EZOSensor::get_device_information() { this->add_command_("i", EzoCommandType::EZO_DEVICE_INFORMATION); } + +void EZOSensor::set_sleep() { this->add_command_("Sleep", EzoCommandType::EZO_SLEEP); } + +void EZOSensor::get_state() { this->add_command_("R", EzoCommandType::EZO_READ, 900); } + +void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_SLOPE); } + +void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); } + +void EZOSensor::set_t(float value) { + std::string payload = str_sprintf("T,%0.2f", value); + this->add_command_(payload, EzoCommandType::EZO_T); +} + +void EZOSensor::set_tempcomp_value(float temp) { this->set_t(temp); } + +void EZOSensor::get_calibration() { this->add_command_("Cal,?", EzoCommandType::EZO_CALIBRATION); } + +void EZOSensor::set_calibration_point_low(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_LOW, value); +} + +void EZOSensor::set_calibration_point_mid(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_MID, value); +} + +void EZOSensor::set_calibration_point_high(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_HIGH, value); +} + +void EZOSensor::set_calibration_generic(float value) { + std::string payload = str_sprintf("Cal,%0.2f", value); + this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); +} + +void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommandType::EZO_CALIBRATION); } + +void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); } + +void EZOSensor::set_led_state(bool on) { + std::string to_send = "L,"; + to_send += on ? "1" : "0"; + this->add_command_(to_send, EzoCommandType::EZO_LED); +} + +void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); } + } // namespace ezo } // namespace esphome diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index d46d193ae7..72e82a574f 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -3,10 +3,35 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include namespace esphome { namespace ezo { +static const char *const TAG = "ezo.sensor"; + +enum EzoCommandType : uint8_t { + EZO_READ = 0, + EZO_LED = 1, + EZO_DEVICE_INFORMATION = 2, + EZO_SLOPE = 3, + EZO_CALIBRATION, + EZO_SLEEP = 4, + EZO_I2C = 5, + EZO_T = 6, + EZO_CUSTOM = 7 +}; + +enum EzoCalibrationType : uint8_t { EZO_CAL_LOW = 0, EZO_CAL_MID = 1, EZO_CAL_HIGH = 2 }; + +class EzoCommand { + public: + std::string command; + uint16_t delay_ms = 0; + bool command_sent = false; + EzoCommandType command_type; +}; + /// This class implements support for the EZO circuits in i2c mode class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -15,13 +40,71 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void update() override; float get_setup_priority() const override { return setup_priority::DATA; }; - void set_tempcomp_value(float temp); + // I2C + void set_address(uint8_t address); + + // Device Information + void get_device_information(); + void add_device_infomation_callback(std::function &&callback) { + this->device_infomation_callback_.add(std::move(callback)); + } + + // Sleep + void set_sleep(); + + // R + void get_state(); + + // Slope + void get_slope(); + void add_slope_callback(std::function &&callback) { + this->slope_callback_.add(std::move(callback)); + } + + // T + void get_t(); + void set_t(float value); + void set_tempcomp_value(float temp); // For backwards compatibility + void add_t_callback(std::function &&callback) { this->t_callback_.add(std::move(callback)); } + + // Calibration + void get_calibration(); + void set_calibration_point_low(float value); + void set_calibration_point_mid(float value); + void set_calibration_point_high(float value); + void set_calibration_generic(float value); + void clear_calibration(); + void add_calibration_callback(std::function &&callback) { + this->calibration_callback_.add(std::move(callback)); + } + + // LED + void get_led_state(); + void set_led_state(bool on); + void add_led_state_callback(std::function &&callback) { this->led_callback_.add(std::move(callback)); } + + // Custom + void send_custom(const std::string &to_send); + void add_custom_callback(std::function &&callback) { + this->custom_callback_.add(std::move(callback)); + } protected: + std::deque> commands_; + int new_address_; + + void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300); + + void set_calibration_point_(EzoCalibrationType type, float value); + + CallbackManager device_infomation_callback_{}; + CallbackManager calibration_callback_{}; + CallbackManager slope_callback_{}; + CallbackManager t_callback_{}; + CallbackManager custom_callback_{}; + CallbackManager led_callback_{}; + uint32_t start_time_ = 0; - uint32_t wait_time_ = 0; - uint16_t state_ = 0; - float tempcomp_; }; } // namespace ezo diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index 09b36b7135..049b1bf4d0 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -1,22 +1,81 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import i2c, sensor -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] DEPENDENCIES = ["i2c"] +CONF_ON_LED = "on_led" +CONF_ON_DEVICE_INFORMATION = "on_device_information" +CONF_ON_SLOPE = "on_slope" +CONF_ON_CALIBRATION = "on_calibration" +CONF_ON_T = "on_t" +CONF_ON_CUSTOM = "on_custom" + ezo_ns = cg.esphome_ns.namespace("ezo") EZOSensor = ezo_ns.class_( "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) +CustomTrigger = ezo_ns.class_( + "CustomTrigger", automation.Trigger.template(cg.std_string) +) + + +TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string)) + +SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string)) + +CalibrationTrigger = ezo_ns.class_( + "CalibrationTrigger", automation.Trigger.template(cg.std_string) +) + +DeviceInformationTrigger = ezo_ns.class_( + "DeviceInformationTrigger", automation.Trigger.template(cg.std_string) +) + +LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) + CONFIG_SCHEMA = ( sensor.SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(EZOSensor), + cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), + } + ), + cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger), + } + ), + cv.Optional(CONF_ON_SLOPE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger), + } + ), + cv.Optional(CONF_ON_T): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger), + } + ), + cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DeviceInformationTrigger + ), + } + ), + cv.Optional(CONF_ON_LED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger), + } + ), } ) .extend(cv.polling_component_schema("60s")) @@ -29,3 +88,27 @@ async def to_code(config): await cg.register_component(var, config) await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) + + for conf in config.get(CONF_ON_CUSTOM, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_LED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "x")], conf) + + for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_SLOPE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_CALIBRATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_T, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf)