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
This commit is contained in:
Nicolas Bachschwell 2024-05-25 20:01:05 +02:00
parent 4b7195a037
commit 1bd3b7eb90
Signed by: NBSgamesAT
GPG Key ID: 2D73288FF7AEED2F
5 changed files with 340 additions and 20 deletions

View File

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

View File

@ -0,0 +1 @@
CODEOWNERS = ["@nbsgames"]

View File

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

View File

@ -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<typename... Ts> class EzoCalibrateAction : public Action<Ts...> {
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<typename... Ts> class DeviceInfoAction : public Action<Ts...> {
public:
DeviceInfoAction(EzoOrpSensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(float, value)
void play(Ts... x) override {
this->sensor_->print_info();
}
protected:
EzoOrpSensor *sensor_;
};
}
}

View File

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