From 6236db1a27d17ea1f0481a51fad13ebaf1528601 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 14 Sep 2022 06:51:20 +0200 Subject: [PATCH] Add uFire ISE sensor (#3789) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ufire_ise/__init__.py | 1 + esphome/components/ufire_ise/sensor.py | 127 +++++++++++++++++ esphome/components/ufire_ise/ufire_ise.cpp | 153 +++++++++++++++++++++ esphome/components/ufire_ise/ufire_ise.h | 95 +++++++++++++ esphome/const.py | 2 + tests/test2.yaml | 5 + tests/test4.yaml | 6 + 8 files changed, 390 insertions(+) create mode 100644 esphome/components/ufire_ise/__init__.py create mode 100644 esphome/components/ufire_ise/sensor.py create mode 100644 esphome/components/ufire_ise/ufire_ise.cpp create mode 100644 esphome/components/ufire_ise/ufire_ise.h diff --git a/CODEOWNERS b/CODEOWNERS index 3a1087191c..69e30027a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core esphome/components/ufire_ec/* @pvizeli +esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/version/* @esphome/core esphome/components/wake_on_lan/* @willwill2will54 diff --git a/esphome/components/ufire_ise/__init__.py b/esphome/components/ufire_ise/__init__.py new file mode 100644 index 0000000000..08f36c7934 --- /dev/null +++ b/esphome/components/ufire_ise/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@pvizeli"] diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py new file mode 100644 index 0000000000..8f4359d6af --- /dev/null +++ b/esphome/components/ufire_ise/sensor.py @@ -0,0 +1,127 @@ +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_PH, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PH, +) + +DEPENDENCIES = ["i2c"] + +CONF_SOLUTION = "solution" +CONF_TEMPERATURE_SENSOR = "temperature_sensor" + +ufire_ise_ns = cg.esphome_ns.namespace("ufire_ise") +UFireISEComponent = ufire_ise_ns.class_( + "UFireISEComponent", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +UFireISECalibrateProbeLowAction = ufire_ise_ns.class_( + "UFireISECalibrateProbeLowAction", automation.Action +) +UFireISECalibrateProbeHighAction = ufire_ise_ns.class_( + "UFireISECalibrateProbeHighAction", automation.Action +) +UFireISEResetAction = ufire_ise_ns.class_("UFireISEResetAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UFireISEComponent), + 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_PH): sensor.sensor_schema( + unit_of_measurement=UNIT_PH, + 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 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3F)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_PH in config: + sens = await sensor.new_sensor(config[CONF_PH]) + cg.add(var.set_ph_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_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireISEComponent), + cv.Required(CONF_SOLUTION): cv.templatable(float), + } +) + + +@automation.register_action( + "ufire_ise.calibrate_probe_low", + UFireISECalibrateProbeLowAction, + UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ise_calibrate_probe_low_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) + template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + cg.add(var.set_solution(template_)) + return var + + +@automation.register_action( + "ufire_ise.calibrate_probe_high", + UFireISECalibrateProbeHighAction, + UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ise_calibrate_probe_high_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) + template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + cg.add(var.set_solution(template_)) + return var + + +UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent)}) + + +@automation.register_action( + "ufire_ise.reset", + UFireISEResetAction, + UFIRE_ISE_RESET_SCHEMA, +) +async def ufire_ise_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_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp new file mode 100644 index 0000000000..957e6f3299 --- /dev/null +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -0,0 +1,153 @@ +#include "esphome/core/log.h" +#include "ufire_ise.h" + +#include + +namespace esphome { +namespace ufire_ise { + +static const char *const TAG = "ufire_ise"; + +void UFireISEComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up uFire_ise..."); + + uint8_t version; + if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + this->mark_failed(); + return; + } + ESP_LOGI(TAG, "Found uFire_ise 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); +} + +void UFireISEComponent::update() { + int wait = 0; + if (this->temperature_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_TEMP); + wait += 750; + } + if (this->ph_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_MV); + wait += 750; + } + + // Wait until measurement are taken + this->set_timeout("data", wait, [this]() { this->update_internal_(); }); +} + +void UFireISEComponent::update_internal_() { + float temperature = 0; + + // Read temperature internal and populate it + if (this->temperature_sensor_ != nullptr) { + temperature = this->measure_temperature_(); + this->temperature_sensor_->publish_state(temperature); + } + // Get temperature from external only for adjustments + else if (this->temperature_sensor_external_ != nullptr) { + temperature = this->temperature_sensor_external_->state; + } + + if (this->ph_sensor_ != nullptr) { + this->ph_sensor_->publish_state(this->measure_ph_(temperature)); + } +} + +float UFireISEComponent::measure_temperature_() { return this->read_data_(REGISTER_TEMP); } + +float UFireISEComponent::measure_mv_() { return this->read_data_(REGISTER_MV); } + +float UFireISEComponent::measure_ph_(float temperature) { + float mv, ph; + + mv = this->measure_mv_(); + if (mv == -1) + return -1; + + ph = fabs(7.0 - (mv / PROBE_MV_TO_PH)); + + // Determine the temperature correction + float distance_from_7 = std::abs(7 - roundf(ph)); + float distance_from_25 = std::floor(std::abs(25 - roundf(temperature)) / 10); + float temp_multiplier = (distance_from_25 * distance_from_7) * PROBE_TMP_CORRECTION; + if ((ph >= 8.0) && (temperature >= 35)) + temp_multiplier *= -1; + if ((ph <= 6.0) && (temperature <= 15)) + temp_multiplier *= -1; + + ph += temp_multiplier; + if ((ph <= 0.0) || (ph > 14.0)) + ph = -1; + if (std::isinf(ph)) + ph = -1; + if (std::isnan(ph)) + ph = -1; + + return ph; +} + +void UFireISEComponent::set_solution_(float solution) { + solution = (7 - solution) * PROBE_MV_TO_PH; + this->write_data_(REGISTER_SOLUTION, solution); +} + +void UFireISEComponent::calibrate_probe_low(float solution) { + this->set_solution_(solution); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_LOW); +} + +void UFireISEComponent::calibrate_probe_high(float solution) { + this->set_solution_(solution); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_HIGH); +} + +void UFireISEComponent::reset_board() { + this->write_data_(REGISTER_REFHIGH, NAN); + this->write_data_(REGISTER_REFLOW, NAN); + this->write_data_(REGISTER_READHIGH, NAN); + this->write_data_(REGISTER_READLOW, NAN); +} + +float UFireISEComponent::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 UFireISEComponent::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 UFireISEComponent::dump_config() { + ESP_LOGCONFIG(TAG, "uFire-ISE"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_) + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) +} + +} // namespace ufire_ise +} // namespace esphome diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h new file mode 100644 index 0000000000..01efdcdb55 --- /dev/null +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -0,0 +1,95 @@ +#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_ise { + +static const float PROBE_MV_TO_PH = 59.2; +static const float PROBE_TMP_CORRECTION = 0.03; + +static const uint8_t CONFIG_TEMP_COMPENSATION = 0x02; + +static const uint8_t REGISTER_VERSION = 0; +static const uint8_t REGISTER_MV = 1; +static const uint8_t REGISTER_TEMP = 5; +static const uint8_t REGISTER_REFHIGH = 13; +static const uint8_t REGISTER_REFLOW = 17; +static const uint8_t REGISTER_READHIGH = 21; +static const uint8_t REGISTER_READLOW = 25; +static const uint8_t REGISTER_SOLUTION = 29; +static const uint8_t REGISTER_CONFIG = 38; +static const uint8_t REGISTER_TASK = 39; + +static const uint8_t COMMAND_CALIBRATE_HIGH = 8; +static const uint8_t COMMAND_CALIBRATE_LOW = 10; +static const uint8_t COMMAND_MEASURE_TEMP = 40; +static const uint8_t COMMAND_MEASURE_MV = 80; + +class UFireISEComponent : 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_ph_sensor(sensor::Sensor *ph_sensor) { this->ph_sensor_ = ph_sensor; } + void calibrate_probe_low(float solution); + void calibrate_probe_high(float solution); + void reset_board(); + + protected: + float measure_temperature_(); + float measure_mv_(); + float measure_ph_(float temperature); + void set_solution_(float solution); + 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 *ph_sensor_{nullptr}; +}; + +template class UFireISECalibrateProbeLowAction : public Action { + public: + UFireISECalibrateProbeLowAction(UFireISEComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + + void play(Ts... x) override { this->parent_->calibrate_probe_low(this->solution_.value(x...)); } + + protected: + UFireISEComponent *parent_; +}; + +template class UFireISECalibrateProbeHighAction : public Action { + public: + UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + + void play(Ts... x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } + + protected: + UFireISEComponent *parent_; +}; + +template class UFireISEResetAction : public Action { + public: + UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset_board(); } + + protected: + UFireISEComponent *parent_; +}; + +} // namespace ufire_ise +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 5153efbbe0..c9d46fc82c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -493,6 +493,7 @@ CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_PERIOD = "period" +CONF_PH = "ph" CONF_PHASE_ANGLE = "phase_angle" CONF_PHASE_BALANCER = "phase_balancer" CONF_PIN = "pin" @@ -881,6 +882,7 @@ UNIT_PARTS_PER_BILLION = "ppb" UNIT_PARTS_PER_MILLION = "ppm" UNIT_PASCAL = "Pa" UNIT_PERCENT = "%" +UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" UNIT_SECOND = "s" diff --git a/tests/test2.yaml b/tests/test2.yaml index f8ed04d389..5507fa0631 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -376,6 +376,11 @@ sensor: temperature_sensor: ha_hello_world_temperature temperature_compensation: 20.0 temperature_coefficient: 0.019 + - platform: ufire_ise + id: ufire_ise_board + temperature_sensor: ha_hello_world_temperature + ph: + name: Ufire pH time: - platform: homeassistant diff --git a/tests/test4.yaml b/tests/test4.yaml index d79b421cf8..6293e0f7b7 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -251,6 +251,12 @@ sensor: name: Ufire EC temperature_compensation: 20.0 temperature_coefficient: 0.019 + - platform: ufire_ise + id: ufire_ise_board + temperature: + name: Ufire Temperature + ph: + name: Ufire pH # # platform sensor.apds9960 requires component apds9960