From e3f2562047847cd849ccd05674929bf2c2238f6f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 8 Sep 2022 12:30:07 +0200 Subject: [PATCH] u-fire EC sensor (#3774) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ufire_ec/__init__.py | 1 + esphome/components/ufire_ec/sensor.py | 126 +++++++++++++++++++++++ esphome/components/ufire_ec/ufire_ec.cpp | 119 +++++++++++++++++++++ esphome/components/ufire_ec/ufire_ec.h | 87 ++++++++++++++++ esphome/const.py | 2 + tests/test2.yaml | 7 ++ tests/test4.yaml | 8 ++ 8 files changed, 351 insertions(+) create mode 100644 esphome/components/ufire_ec/__init__.py create mode 100644 esphome/components/ufire_ec/sensor.py create mode 100644 esphome/components/ufire_ec/ufire_ec.cpp create mode 100644 esphome/components/ufire_ec/ufire_ec.h diff --git a/CODEOWNERS b/CODEOWNERS index f8efe79c81..ff497c35ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -241,6 +241,7 @@ esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core +esphome/components/ufire_ec/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/version/* @esphome/core esphome/components/wake_on_lan/* @willwill2will54 diff --git a/esphome/components/ufire_ec/__init__.py b/esphome/components/ufire_ec/__init__.py new file mode 100644 index 0000000000..08f36c7934 --- /dev/null +++ b/esphome/components/ufire_ec/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@pvizeli"] diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py new file mode 100644 index 0000000000..9602d0c2d0 --- /dev/null +++ b/esphome/components/ufire_ec/sensor.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_EC, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MILLISIEMENS_PER_CENTIMETER, +) + +DEPENDENCIES = ["i2c"] + +CONF_SOLUTION = "solution" +CONF_TEMPERATURE_SENSOR = "temperature_sensor" +CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" +CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" + +ufire_ec_ns = cg.esphome_ns.namespace("ufire_ec") +UFireECComponent = ufire_ec_ns.class_( + "UFireECComponent", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +UFireECCalibrateProbeAction = ufire_ec_ns.class_( + "UFireECCalibrateProbeAction", automation.Action +) +UFireECResetAction = ufire_ec_ns.class_("UFireECResetAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UFireECComponent), + cv.Exclusive(CONF_TEMPERATURE, "temperature"): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Optional(CONF_EC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISIEMENS_PER_CENTIMETER, + icon=ICON_EMPTY, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Exclusive(CONF_TEMPERATURE_SENSOR, "temperature"): cv.use_id( + sensor.Sensor + ), + cv.Optional(CONF_TEMPERATURE_COMPENSATION, default=21.0): cv.temperature, + cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0.019): cv.float_range( + min=0.01, max=0.04 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_temperature_compensation(config[CONF_TEMPERATURE_COMPENSATION])) + cg.add(var.set_temperature_coefficient(config[CONF_TEMPERATURE_COEFFICIENT])) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_EC in config: + sens = await sensor.new_sensor(config[CONF_EC]) + cg.add(var.set_ec_sensor(sens)) + + if CONF_TEMPERATURE_SENSOR in config: + sens = await cg.get_variable(config[CONF_TEMPERATURE_SENSOR]) + cg.add(var.set_temperature_sensor_external(sens)) + + await i2c.register_i2c_device(var, config) + + +UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireECComponent), + cv.Required(CONF_SOLUTION): cv.templatable(float), + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + } +) + + +@automation.register_action( + "ufire_ec.calibrate_probe", + UFireECCalibrateProbeAction, + UFIRE_EC_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + solution_ = await cg.templatable(config[CONF_SOLUTION], args, float) + temperature_ = await cg.templatable(config[CONF_TEMPERATURE], args, float) + cg.add(var.set_solution(solution_)) + cg.add(var.set_temperature(temperature_)) + return var + + +UFIRE_EC_RESET_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireECComponent), + } +) + + +@automation.register_action( + "ufire_ec.reset", + UFireECResetAction, + UFIRE_EC_RESET_SCHEMA, +) +async def ufire_ec_reset_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp new file mode 100644 index 0000000000..d7ed890e21 --- /dev/null +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -0,0 +1,119 @@ +#include "esphome/core/log.h" +#include "ufire_ec.h" + +namespace esphome { +namespace ufire_ec { + +static const char *const TAG = "ufire_ec"; + +void UFireECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up uFire_ec..."); + + uint8_t version; + if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + this->mark_failed(); + this->status_set_error(); + return; + } + ESP_LOGI(TAG, "Found ufire_ec board version 0x%02X", version); + + // Write option for temperature adjustments + uint8_t config; + this->read_byte(REGISTER_CONFIG, &config); + if (this->temperature_sensor_ == nullptr && this->temperature_sensor_external_ == nullptr) { + config &= ~CONFIG_TEMP_COMPENSATION; + } else { + config |= CONFIG_TEMP_COMPENSATION; + } + this->write_byte(REGISTER_CONFIG, config); + + // Update temperature compensation + this->set_compensation_(this->temperature_compensation_); + this->set_coefficient_(this->temperature_coefficient_); +} + +void UFireECComponent::update() { + int wait = 0; + + if (this->temperature_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_TEMP); + wait += 750; + } else if (this->temperature_sensor_external_ != nullptr) { + this->set_temperature_(this->temperature_sensor_external_->state); + } + + if (this->ec_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_EC); + wait += 750; + } + + if (wait > 0) { + this->set_timeout("data", wait, [this]() { this->update_internal_(); }); + } +} + +void UFireECComponent::update_internal_() { + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(this->measure_temperature_()); + if (this->ec_sensor_ != nullptr) + this->ec_sensor_->publish_state(this->measure_ms_()); +} + +float UFireECComponent::measure_temperature_() { return this->read_data_(REGISTER_TEMP); } + +float UFireECComponent::measure_ms_() { return this->read_data_(REGISTER_MS); } + +void UFireECComponent::set_solution_(float solution, float temperature) { + solution /= (1 - (this->temperature_coefficient_ * (temperature - 25))); + this->write_data_(REGISTER_SOLUTION, solution); +} + +void UFireECComponent::set_compensation_(float temperature) { this->write_data_(REGISTER_COMPENSATION, temperature); } + +void UFireECComponent::set_coefficient_(float coefficient) { this->write_data_(REGISTER_COEFFICENT, coefficient); } + +void UFireECComponent::set_temperature_(float temperature) { this->write_data_(REGISTER_TEMP, temperature); } + +void UFireECComponent::calibrate_probe(float solution, float temperature) { + this->set_solution_(solution, temperature); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_PROBE); +} + +void UFireECComponent::reset_board() { this->write_data_(REGISTER_CALIBRATE_OFFSET, NAN); } + +float UFireECComponent::read_data_(uint8_t reg) { + float f; + uint8_t temp[4]; + + this->write(®, 1); + delay(10); + + for (uint8_t i = 0; i < 4; i++) { + this->read_bytes_raw(temp + i, 1); + } + memcpy(&f, temp, sizeof(f)); + + return f; +} + +void UFireECComponent::write_data_(uint8_t reg, float data) { + uint8_t temp[4]; + + memcpy(temp, &data, sizeof(data)); + this->write_bytes(reg, temp, 4); + delay(10); +} + +void UFireECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "uFire-EC"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_) + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + ESP_LOGCONFIG(TAG, " Temperature Compensation: %f", this->temperature_compensation_); + ESP_LOGCONFIG(TAG, " Temperature Coefficient: %f", this->temperature_coefficient_); +} + +} // namespace ufire_ec +} // namespace esphome diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h new file mode 100644 index 0000000000..3d436555a2 --- /dev/null +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ufire_ec { + +static const uint8_t CONFIG_TEMP_COMPENSATION = 0x02; + +static const uint8_t REGISTER_VERSION = 0; +static const uint8_t REGISTER_MS = 1; +static const uint8_t REGISTER_TEMP = 5; +static const uint8_t REGISTER_SOLUTION = 9; +static const uint8_t REGISTER_COEFFICENT = 13; +static const uint8_t REGISTER_CALIBRATE_OFFSET = 33; +static const uint8_t REGISTER_COMPENSATION = 45; +static const uint8_t REGISTER_CONFIG = 54; +static const uint8_t REGISTER_TASK = 55; + +static const uint8_t COMMAND_CALIBRATE_PROBE = 20; +static const uint8_t COMMAND_MEASURE_TEMP = 40; +static const uint8_t COMMAND_MEASURE_EC = 80; + +class UFireECComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_temperature_sensor_external(sensor::Sensor *temperature_sensor) { + this->temperature_sensor_external_ = temperature_sensor; + } + void set_ec_sensor(sensor::Sensor *ec_sensor) { this->ec_sensor_ = ec_sensor; } + void set_temperature_compensation(float compensation) { this->temperature_compensation_ = compensation; } + void set_temperature_coefficient(float coefficient) { this->temperature_coefficient_ = coefficient; } + void calibrate_probe(float solution, float temperature); + void reset_board(); + + protected: + float measure_temperature_(); + float measure_ms_(); + void set_solution_(float solution, float temperature); + void set_compensation_(float temperature); + void set_coefficient_(float coefficient); + void set_temperature_(float temperature); + float read_data_(uint8_t reg); + void write_data_(uint8_t reg, float data); + void update_internal_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_external_{nullptr}; + sensor::Sensor *ec_sensor_{nullptr}; + float temperature_compensation_{0.0}; + float temperature_coefficient_{0.0}; +}; + +template class UFireECCalibrateProbeAction : public Action { + public: + UFireECCalibrateProbeAction(UFireECComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + TEMPLATABLE_VALUE(float, temperature) + + void play(Ts... x) override { + this->parent_->calibrate_probe(this->solution_.value(x...), this->temperature_.value(x...)); + } + + protected: + UFireECComponent *parent_; +}; + +template class UFireECResetAction : public Action { + public: + UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset_board(); } + + protected: + UFireECComponent *parent_; +}; + +} // namespace ufire_ec +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index adbd20c620..51c07ab666 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -192,6 +192,7 @@ CONF_DUMMY_RECEIVER_ID = "dummy_receiver_id" CONF_DUMP = "dump" CONF_DURATION = "duration" CONF_EAP = "eap" +CONF_EC = "ec" CONF_ECHO_PIN = "echo_pin" CONF_ECO2 = "eco2" CONF_EFFECT = "effect" @@ -871,6 +872,7 @@ UNIT_MICROSIEMENS_PER_CENTIMETER = "µS/cm" UNIT_MICROTESLA = "µT" UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" UNIT_MILLISECOND = "ms" +UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm" UNIT_MINUTE = "min" UNIT_OHM = "Ω" UNIT_PARTS_PER_BILLION = "ppb" diff --git a/tests/test2.yaml b/tests/test2.yaml index d01a0a742a..f8ed04d389 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -369,6 +369,13 @@ sensor: name: Propane test distance battery_level: name: Propane test battery level + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: ha_hello_world_temperature + temperature_compensation: 20.0 + temperature_coefficient: 0.019 time: - platform: homeassistant diff --git a/tests/test4.yaml b/tests/test4.yaml index d6857de7cd..d79b421cf8 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -243,6 +243,14 @@ sensor: - platform: copy source_id: mcp_sensor name: MCP binary sensor copy + - platform: ufire_ec + id: ufire_ec_board + temperature: + name: Ufire Temperature + ec: + name: Ufire EC + temperature_compensation: 20.0 + temperature_coefficient: 0.019 # # platform sensor.apds9960 requires component apds9960