From 1bd3b7eb903b45908a2723910ac4fd6d78fecbde Mon Sep 17 00:00:00 2001 From: Nicolas Bachschwell Date: Sat, 25 May 2024 20:01:05 +0200 Subject: [PATCH] Changed chlorine-pump to use the Ezo Orp module by AtlasScientific using I2C. This module is by miles more accurate than any analog crap we could have come up with... Not that the last idea was anything to scoff at I guess --- chlorPump.yaml | 48 +++--- external_components/ezo_orp_i2c/__init__.py | 1 + external_components/ezo_orp_i2c/ezo_orp.cpp | 176 ++++++++++++++++++++ external_components/ezo_orp_i2c/ezo_orp.h | 73 ++++++++ external_components/ezo_orp_i2c/sensor.py | 62 +++++++ 5 files changed, 340 insertions(+), 20 deletions(-) create mode 100644 external_components/ezo_orp_i2c/__init__.py create mode 100644 external_components/ezo_orp_i2c/ezo_orp.cpp create mode 100644 external_components/ezo_orp_i2c/ezo_orp.h create mode 100644 external_components/ezo_orp_i2c/sensor.py diff --git a/chlorPump.yaml b/chlorPump.yaml index 591ccf7..60f413a 100644 --- a/chlorPump.yaml +++ b/chlorPump.yaml @@ -21,7 +21,7 @@ external_components: - source: type: local path: external_components/ - components: [ chlorine_pump ] + components: [ chlorine_pump, ezo_orp_i2c] globals: - id: last_pump_state @@ -58,7 +58,7 @@ binary_sensor: - platform: gpio id: pool_pump pin: - number: D2 + number: D5 inverted: true mode: input: true @@ -67,6 +67,24 @@ binary_sensor: - script.execute: id: manage_power switch_state: !lambda return id(power).state; + - platform: gpio + id: calibrater + pin: + number: GPIO0 + inverted: true + on_press: + - sensor.ezo_orp_i2c.print_device_info: + id: chlorine_sensor + on_click: + min_length: 5s + max_length: 10s + then: + - sensor.ezo_orp_i2c.calibrate: + id: chlorine_sensor + calibrate_target: 475 + - output.turn_on: wifi_status_led + - delay: 2s + - output.turn_off: wifi_status_led - platform: template id: pump_state name: Pump State @@ -138,27 +156,17 @@ text_sensor: name: Cycle Info id: cycle_text_info +i2c: + sensor: - - platform: adc - name: Chlorine + - platform: ezo_orp_i2c id: chlorine_sensor - unit_of_measurement: "mV" - icon: "mdi:test-tube" - pin: A0 - update_interval: 200ms - filters: - - multiply: 3.3 - - offset: -1.5 - - multiply: 1000.0 - - median: - window_size: 25 - send_every: 25 - send_first_at: 25 - - offset: 30.0 + name: Chlorine + update_interval: 5s - platform: hx711 name: "Chlorine Canister Levels" - dout_pin: D5 - clk_pin: D1 + dout_pin: D7 + clk_pin: D8 gain: 128 update_interval: 20s accuracy_decimals: 1 @@ -177,7 +185,7 @@ chlorine_pump: pump: pump_switch sensor: chlorine_sensor id: chlorine_pump_component - target: 700 + target: 600 disable_clock: false proportional_band: 400 on_pump_value: diff --git a/external_components/ezo_orp_i2c/__init__.py b/external_components/ezo_orp_i2c/__init__.py new file mode 100644 index 0000000..3da9d9e --- /dev/null +++ b/external_components/ezo_orp_i2c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@nbsgames"] \ No newline at end of file diff --git a/external_components/ezo_orp_i2c/ezo_orp.cpp b/external_components/ezo_orp_i2c/ezo_orp.cpp new file mode 100644 index 0000000..fad30f1 --- /dev/null +++ b/external_components/ezo_orp_i2c/ezo_orp.cpp @@ -0,0 +1,176 @@ +#include "ezo_orp.h" + +namespace esphome { +namespace ezo_orp_i2c { + +static const char *const TAG = "ezo_orp_i2c.sensor"; + +static const uint8_t COMMAND_READ[] = {0x52}; // R +static const uint8_t COMMAND_PERFERM_CALIBRATION[] = {0x43, 0x61, 0x6C}; //Cal +static const uint8_t COMMAND_IS_CALIBRATED[] = {0x43, 0x61, 0x6C, 0x2C, 0x3F}; //Cal +static const uint8_t COMMAND_EXPORT_CALIBRATION[] = {0x45, 0x78, 0x70, 0x6F, 0x72, 0x74}; // Export +static const uint8_t COMMAND_INFORMATION[] = {0x69}; // i (DON'T ASK ME WHY THAT IS ONLY A SMALL LETTER +static const uint8_t COMMAND_STATUS[] = {0x53, 0x74, 0x61, 0x74, 0x75, 0x73}; // STATUS + +static const uint8_t COMMAND_FACTORY_RESET[] = {0x46, 0x61, 0x63, 0x74, 0x6F, 0x72, 0x79}; // Factory + +static const uint8_t COMMMAND_TO_ARGS_SEPERATOR = {0x2C}; + +static const int PROCESSING_DELAY_MS = 300; // FOR SOME OPERATIONS +static const int READING_DELAY_MS = 900; // FOR BASICALLY EVERYTHING WHERE LARGE DATA IS READ/SENT + + +void EzoOrpSensor::setup(){ + ESP_LOGCONFIG(TAG, "Setting up EzoOrpI2C '%s'...", this->get_name().c_str()); + ESP_LOGCONFIG(TAG, "EzoOrpI2C '%s' setup finished!", this->get_name().c_str()); + scheduleNextAction(SensorAction::IS_CALIBRATED, nullptr, 0, 450); +} + +void EzoOrpSensor::update() { + scheduleNextAction(SensorAction::READ, nullptr, 0, 900); +} + +void EzoOrpSensor::read_response(int maxLength, SensorAction action) { + uint8_t data[maxLength]; + + if(SensorAction::CALIBRATE == action){ + ESP_LOGD(TAG, "Device Calibrated"); + busy_ = false; + runScheduledAction(); + return; + } + + if(i2c::ERROR_OK != this->read(data, maxLength)){ + ESP_LOGW(TAG, "Error Occured while reading respnse!"); + return; + } + + if(data[0] != 1 && data[0] != 255){ + ESP_LOGW(TAG, "Read status code of %d, (Operation: %d)", data[0], action); + return; + } + + if(SensorAction::READ == action){ + float value; + sscanf((const char*) &data[1], "%f", &value); + this->publish_state(value); + } + else if(SensorAction::IS_CALIBRATED == action){ + ESP_LOGD(TAG, "%s", data[6] == 0x31 ? "Device has been calibrated" : "Device has NOT been calibrated"); + } + else if(SensorAction::INFORMATION == action){ + ESP_LOGD(TAG, "Device Printed the following information: %s", &data[1]); + } + + busy_ = false; + + runScheduledAction(); +} + +void EzoOrpSensor::calibrate_to_target(int target){ + char targetArray[3]; + sprintf(targetArray, "%d", target); + ESP_LOGD(TAG, "Calibrating to: %3s", targetArray); + scheduleNextAction(SensorAction::CALIBRATE, (uint8_t*) &targetArray, 3, 4000); + +} + +float EzoOrpSensor::get_setup_priority() const { return setup_priority::DATA; } + +void EzoOrpSensor::dump_config(){ + LOG_SENSOR("", "EzoOrpI2C Sensor", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} + +bool EzoOrpSensor::is_busy(){ + return this->busy_; +} + +void EzoOrpSensor::runScheduledAction(){ + if(scheduledAction_ == SensorAction::NOTHING) return; + scheduleNextAction(scheduledAction_, scheduledActionData_, scheduledActionDataSize_, scheduledActionTime_); + scheduledAction_ = SensorAction::NOTHING; + scheduledActionDataSize_ = 0; +} + +void EzoOrpSensor::scheduleNextAction(SensorAction action, uint8_t* data, int size, int expected_response_time){ + if(busy_){ + if(scheduledAction_ == action) return; // Queued Action cannot has no need of being repeated; + if(scheduledAction_ != SensorAction::NOTHING) return; // Ehh... Felt better lol + + scheduledAction_ = action; + if(data != nullptr){ + for(int i = 0;i < size; ++i){ + scheduledActionData_[i] = data[i]; + } + } + scheduledActionDataSize_ = size; + scheduledActionTime_ = expected_response_time; + return; + } + + switch(action){ + case SensorAction::NOTHING: + return; + case SensorAction::READ: + busy_ = true; + send_command(COMMAND_READ, sizeof(COMMAND_READ)); + this->set_timeout(expected_response_time, [this](){ this->read_response(9, SensorAction::READ); }); + break; + case SensorAction::CALIBRATE: + busy_ = true; + send_command(COMMAND_PERFERM_CALIBRATION, sizeof(COMMAND_PERFERM_CALIBRATION), scheduledActionData_, scheduledActionDataSize_); + this->set_timeout(expected_response_time, [this](){ this->read_response(2, SensorAction::CALIBRATE); }); + break; + case SensorAction::IS_CALIBRATED: + busy_ = true; + send_command(COMMAND_IS_CALIBRATED, sizeof(COMMAND_IS_CALIBRATED)); + this->set_timeout(expected_response_time, [this](){ this->read_response(8, SensorAction::IS_CALIBRATED); }); + break; + case SensorAction::INFORMATION: + busy_ = true; + send_command(COMMAND_STATUS, sizeof(COMMAND_STATUS)); + this->set_timeout(expected_response_time, [this](){ this->read_response(18, SensorAction::INFORMATION); }); + break; + default: + break; + } + +} + +bool EzoOrpSensor::send_command(const uint8_t* data, size_t dataSize){ + return send_command(data, dataSize, nullptr, 0); +} +bool EzoOrpSensor::send_command(const uint8_t* data, size_t dataSize, const uint8_t* parameterData, size_t parameterSize){ + + if(parameterData == nullptr){ + if(i2c::ERROR_OK != this->write(data, dataSize)){ + ESP_LOGW(TAG, "send_command failed writing data!"); + return false; + } + return true; + } + + uint8_t completeData[dataSize + 1 + parameterSize]; + for(int i = 0;i < dataSize; ++i){ + completeData[i] = data[i]; + } + completeData[dataSize++] = COMMMAND_TO_ARGS_SEPERATOR; + for(int i = 0; i < parameterSize; ++i){ + completeData[dataSize + i] = parameterData[i]; + } + + if(i2c::ERROR_OK != this->write(completeData, sizeof(completeData))){ + ESP_LOGW(TAG, "send_command failed writing data!"); + return false; + } + return true; +} + +void EzoOrpSensor::print_info(){ + scheduleNextAction(SensorAction::INFORMATION, nullptr, 0, 300); +} + +} +} \ No newline at end of file diff --git a/external_components/ezo_orp_i2c/ezo_orp.h b/external_components/ezo_orp_i2c/ezo_orp.h new file mode 100644 index 0000000..f44246a --- /dev/null +++ b/external_components/ezo_orp_i2c/ezo_orp.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/automation.h" +#include "Arduino.h" + +namespace esphome { +namespace ezo_orp_i2c { + +enum SensorAction: int{ + NOTHING = 0, + READ = 10, + CALIBRATE = 20, + IS_CALIBRATED = 21, + EXPORT_CALIBRATION = 30, + FACTORY_RESET = 40, + INFORMATION = 50, +}; + +class EzoOrpSensor: public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const; + void calibrate_to_target(int target); + void print_info(); + bool is_busy(); + protected: + SensorAction scheduledAction_ = SensorAction::NOTHING; // THIS ACTS AS A SORT OF QUEUE + uint8_t scheduledActionData_[20] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int scheduledActionDataSize_ = 0; + int scheduledActionTime_ = 0; + bool busy_ = false; + void read_response(int maxLength, SensorAction action); + void runScheduledAction(); + void scheduleNextAction(SensorAction action, uint8_t* data, int size, int expected_response_time); + bool send_command(const uint8_t* data, size_t dataSize); + bool send_command(const uint8_t* data, size_t dataSize, const uint8_t* parameterData, size_t parameterSize); + //bool read_data_(u_int8_t[] * read_data); +}; + + +template class EzoCalibrateAction : public Action { + public: + EzoCalibrateAction(EzoOrpSensor *sensor) : sensor_(sensor) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + this->sensor_->calibrate_to_target((int) this->value_.value(x...)); + } + + protected: + EzoOrpSensor *sensor_; +}; + +template class DeviceInfoAction : public Action { + public: + DeviceInfoAction(EzoOrpSensor *sensor) : sensor_(sensor) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + this->sensor_->print_info(); + } + + protected: + EzoOrpSensor *sensor_; +}; + +} +} \ No newline at end of file diff --git a/external_components/ezo_orp_i2c/sensor.py b/external_components/ezo_orp_i2c/sensor.py new file mode 100644 index 0000000..7a0e5a7 --- /dev/null +++ b/external_components/ezo_orp_i2c/sensor.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import sensor, i2c +from esphome.components.adc import sensor as adc +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] + +chlorine_sensor_ns = cg.esphome_ns.namespace('ezo_orp_i2c') +ChlorineSensor = chlorine_sensor_ns.class_('EzoOrpSensor', cg.PollingComponent, sensor.Sensor, i2c.I2CDevice) + +UNIT_MILLI_VOLT = "mV" + +ICON_TEST_TUBE="mdi:test-tube" + +ACTION_CONF_CALIBRATE_TARGET = "calibrate_target" + +CalibrateAction = chlorine_sensor_ns.class_("EzoCalibrateAction", automation.Action) +DeviceInfoAction = chlorine_sensor_ns.class_("DeviceInfoAction", automation.Action) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + ChlorineSensor, + unit_of_measurement=UNIT_MILLI_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_TEST_TUBE + ) + .extend(i2c.i2c_device_schema(0x62)) + .extend(cv.polling_component_schema("60s")) +) + +ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(ChlorineSensor) +}) + +@automation.register_action("sensor.ezo_orp_i2c.calibrate", CalibrateAction, ACTION_SCHEMA.extend({ + cv.Required(ACTION_CONF_CALIBRATE_TARGET): cv.int_range(100, 999), +})) +async def calibrate_action_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[ACTION_CONF_CALIBRATE_TARGET], args, float) + cg.add(var.set_value(template_)) + return var + +@automation.register_action("sensor.ezo_orp_i2c.print_device_info", DeviceInfoAction, ACTION_SCHEMA) +async def info_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config)