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:
parent
4b7195a037
commit
1bd3b7eb90
@ -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:
|
||||
|
1
external_components/ezo_orp_i2c/__init__.py
Normal file
1
external_components/ezo_orp_i2c/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@nbsgames"]
|
176
external_components/ezo_orp_i2c/ezo_orp.cpp
Normal file
176
external_components/ezo_orp_i2c/ezo_orp.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
73
external_components/ezo_orp_i2c/ezo_orp.h
Normal file
73
external_components/ezo_orp_i2c/ezo_orp.h
Normal 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_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
62
external_components/ezo_orp_i2c/sensor.py
Normal file
62
external_components/ezo_orp_i2c/sensor.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user