Enable calibration, callbacks and custom commands for EZO sensors (#3910)

Co-authored-by: PoppyPop <skytep@gmail.com>
Co-authored-by: Samuel Sieb <samuel@sieb.net>
This commit is contained in:
Gilles van den Hoven 2022-11-09 04:46:31 +01:00 committed by GitHub
parent 02b15dbc4a
commit dc794918ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 414 additions and 43 deletions

View file

@ -0,0 +1,53 @@
#pragma once
#include <utility>
#include "esphome/core/automation.h"
#include "ezo.h"
namespace esphome {
namespace ezo {
class LedTrigger : public Trigger<bool> {
public:
explicit LedTrigger(EZOSensor *ezo) {
ezo->add_led_state_callback([this](bool value) { this->trigger(value); });
}
};
class CustomTrigger : public Trigger<std::string> {
public:
explicit CustomTrigger(EZOSensor *ezo) {
ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class TTrigger : public Trigger<std::string> {
public:
explicit TTrigger(EZOSensor *ezo) {
ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class CalibrationTrigger : public Trigger<std::string> {
public:
explicit CalibrationTrigger(EZOSensor *ezo) {
ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class SlopeTrigger : public Trigger<std::string> {
public:
explicit SlopeTrigger(EZOSensor *ezo) {
ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class DeviceInformationTrigger : public Trigger<std::string> {
public:
explicit DeviceInformationTrigger(EZOSensor *ezo) {
ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
} // namespace ezo
} // namespace esphome

View file

@ -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<EzoCommand> 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<const uint8_t *>(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<char *>(&buf[1]);
if (!payload.empty()) {
switch (to_run->command_type) {
case EzoCommandType::EZO_READ: {
auto val = parse_number<float>(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<float>((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<EzoCommand> 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

View file

@ -3,10 +3,35 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include <deque>
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<void(std::string)> &&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<void(std::string)> &&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<void(std::string)> &&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<void(std::string)> &&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<void(bool)> &&callback) { this->led_callback_.add(std::move(callback)); }
// Custom
void send_custom(const std::string &to_send);
void add_custom_callback(std::function<void(std::string)> &&callback) {
this->custom_callback_.add(std::move(callback));
}
protected:
std::deque<std::unique_ptr<EzoCommand>> 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<void(std::string)> device_infomation_callback_{};
CallbackManager<void(std::string)> calibration_callback_{};
CallbackManager<void(std::string)> slope_callback_{};
CallbackManager<void(std::string)> t_callback_{};
CallbackManager<void(std::string)> custom_callback_{};
CallbackManager<void(bool)> led_callback_{};
uint32_t start_time_ = 0;
uint32_t wait_time_ = 0;
uint16_t state_ = 0;
float tempcomp_;
};
} // namespace ezo

View file

@ -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)