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)