From 3d7e80154a9ca08feb2620c32c4497a75e90c7d7 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschwell Date: Sun, 12 May 2024 23:02:52 +0200 Subject: [PATCH] All the code changes to the chlorine pump... More will follow --- chlorPump.yaml | 143 +++++++++++- external_components/analog_orp/__init__.py | 1 + external_components/analog_orp/analog_orp.cpp | 3 + external_components/analog_orp/analog_orp.h | 3 +- external_components/analog_orp/sensor.py | 10 +- external_components/chlorine_pump/__init__.py | 128 +++++++++++ .../chlorine_pump/pump_cycle.h | 216 ++++++++++++++++++ 7 files changed, 489 insertions(+), 15 deletions(-) create mode 100644 external_components/chlorine_pump/__init__.py create mode 100644 external_components/chlorine_pump/pump_cycle.h diff --git a/chlorPump.yaml b/chlorPump.yaml index 973620d..f76e5e5 100644 --- a/chlorPump.yaml +++ b/chlorPump.yaml @@ -3,6 +3,10 @@ esphome: project: name: nbsgamesat.chlorine-pump version: "0.2" + on_boot: + priority: 600 + then: + output.turn_on: whatever esp8266: board: nodemcuv2 @@ -17,7 +21,15 @@ external_components: - source: type: local path: external_components/ - components: [ analog_orp ] + components: [ analog_orp, chlorine_pump ] + +globals: + - id: last_pump_state + type: bool + restore_value: no + initial_value: "false" + - id: last_cycle_info + type: std::string api: encryption: @@ -27,31 +39,144 @@ wifi: ssid: !secret wifi_ssid password: !secret wifi_password fast_connect: true + on_connect: + output.turn_off: whatever output: - platform: gpio - pin: D3 + pin: D6 id: pump_switch + - platform: gpio + pin: + number: D0 + inverted: true + id: whatever binary_sensor: - platform: gpio id: pool_pump - pin: D2 + pin: + number: D2 + inverted: true + mode: + input: true + pullup: true + on_state: + - script.execute: + id: manage_power + switch_state: !lambda return id(power).state; + - platform: template + id: pump_state + name: Pump State + lambda: !lambda return id(last_pump_state); + +script: + - id: manage_power + parameters: + switch_state: bool + then: + - if: + condition: + lambda: |- + return switch_state && id(pool_pump).state; + then: + - chlorine_pump.start: {} + - text_sensor.template.publish: + id: cycle_text_info + state: !lambda 'return "ON... ";' + else: + - chlorine_pump.stop: {} + - text_sensor.template.publish: + id: cycle_text_info + state: !lambda 'return "PUMP: OFF";' + +switch: + - platform: template + name: Chlorine Pump Power + id: power + optimistic: true + inverted: off + restore_mode: RESTORE_DEFAULT_ON + turn_on_action: + - script.execute: + id: manage_power + switch_state: true + turn_off_action: + - script.execute: + id: manage_power + switch_state: false + +button: + - platform: template + name: Prime + id: prime + on_press: + - chlorine_pump.prime: {} + - text_sensor.template.publish: + id: cycle_text_info + state: !lambda 'return "PRIMING??";' + +number: + - platform: template + name: Chlorine Target + id: chlorine_target + initial_value: 700 + restore_value: true + min_value: 300 + max_value: 1400 + optimistic: true + step: 1.0 + set_action: + then: + - chlorine_pump.set_target: + target: !lambda return x; + +text_sensor: + - platform: template + name: Cycle Info + id: cycle_text_info sensor: - platform: analog_orp name: Chlorine id: chlorine_sensor pin: A0 - zero_point: 640 + zero_point: 673 inverted: true update_interval: 1s +# print_raw: true average: - mesurements: 5 + mesurements: 15 send_state_every: 10 - on_completely_new_value: - - lambda: |- - ESP_LOGD("WHAT", "Just a test %.1f", value); -# GPIO A0 to acd +# on_value_read: +# - lambda: |- +# ESP_LOGD("WHAT", "Chlorine_value, %.1f", x); + - platform: hx711 + name: "Chlorine Canister Levels" + dout_pin: D5 + clk_pin: D1 + gain: 128 + update_interval: 20s + accuracy_decimals: 1 + filters: + - calibrate_linear: + - 47608 -> 0 + - 590566 -> 100 + unit_of_measurement: "%" + +chlorine_pump: + pump: pump_switch + sensor: chlorine_sensor + id: chlorine_pump_component + target: 700 + disable_clock: true + on_pump_value: + - lambda: |- + if(pState != id(last_pump_state)){ + id(last_pump_state) = pState; + } + on_cycle_start: + - lambda: |- + id(cycle_text_info).publish_state("tOn: " + std::to_string(tOn) + "s\n tOff: " + std::to_string(tOff) + "s"); # Here is space for any display implementation that could possibliy be. Have fun OK \ No newline at end of file diff --git a/external_components/analog_orp/__init__.py b/external_components/analog_orp/__init__.py index e69de29..3da9d9e 100644 --- a/external_components/analog_orp/__init__.py +++ b/external_components/analog_orp/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@nbsgames"] \ No newline at end of file diff --git a/external_components/analog_orp/analog_orp.cpp b/external_components/analog_orp/analog_orp.cpp index 1bd3470..eb25c6e 100644 --- a/external_components/analog_orp/analog_orp.cpp +++ b/external_components/analog_orp/analog_orp.cpp @@ -109,6 +109,9 @@ void ChlorineSensor::dump_config(){ ESP_LOGCONFIG(TAG, " Inverted: %s", inverted_ ? "true" : "false"); ESP_LOGCONFIG(TAG, " Print Raw: %s", print_raw_ ? "true" : "false"); } +bool ChlorineSensor::has_averager(){ + return averager_ != nullptr; +} /* Nullwert : 680 diff --git a/external_components/analog_orp/analog_orp.h b/external_components/analog_orp/analog_orp.h index 6c210bd..d9ac402 100644 --- a/external_components/analog_orp/analog_orp.h +++ b/external_components/analog_orp/analog_orp.h @@ -1,10 +1,10 @@ #pragma once -#include "esphome.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" #include "esphome/core/gpio.h" #include "esphome/core/automation.h" +#include "Arduino.h" namespace esphome { namespace analog_orp { @@ -33,6 +33,7 @@ class ChlorineSensor: public PollingComponent, public sensor::Sensor { void add_average_change_callback(std::function &&callback); void add_new_value_callback(std::function &&callback); void init_averager(int mesurements, int send_average_every); + bool has_averager(); void setup() override; void update() override; float sample(); diff --git a/external_components/analog_orp/sensor.py b/external_components/analog_orp/sensor.py index 529e73c..310e25d 100644 --- a/external_components/analog_orp/sensor.py +++ b/external_components/analog_orp/sensor.py @@ -26,7 +26,7 @@ CONF_AVERAGE_NEW = "on_completely_new_value" UNIT_MILLI_VOLT = "mV" -ICON_LIGHTNING_BOLT="mdi:lightning-bolt" +ICON_TEST_TUBE="mdi:test-tube" ChlorineValueRead = chlorine_sensor_ns.class_( "ChlorineValueReadTrigger", automation.Trigger.template(cg.float_) @@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All( accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, - icon=ICON_LIGHTNING_BOLT + icon=ICON_TEST_TUBE ).extend({ cv.Required(CONF_SENSOR_PIN): adc.validate_adc_pin, cv.Required(CONF_ZERO_POINT): cv.Range(min=0, max=1023), @@ -88,12 +88,12 @@ async def to_code(config): for conf in averager_config.get(CONF_ON_READ_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "value")], conf) + await automation.build_automation(trigger, [(float, "x")], conf) for conf in averager_config.get(CONF_AVERAGE_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "value")], conf) + await automation.build_automation(trigger, [(float, "x")], conf) for conf in averager_config.get(CONF_AVERAGE_NEW, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "value")], conf) \ No newline at end of file + await automation.build_automation(trigger, [(float, "x")], conf) \ No newline at end of file diff --git a/external_components/chlorine_pump/__init__.py b/external_components/chlorine_pump/__init__.py new file mode 100644 index 0000000..fedcac0 --- /dev/null +++ b/external_components/chlorine_pump/__init__.py @@ -0,0 +1,128 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import output +from esphome.components.analog_orp.sensor import ChlorineSensor +from esphome.const import ( + CONF_ID, + CONF_TRIGGER_ID +) + + +DEPENDENCIES=["sensor"] + +chlorine_pump_ns = cg.esphome_ns.namespace('chlorine_pump') +ChlorinePump = chlorine_pump_ns.class_('ChlorinePump', cg.Component) + +CONF_SENSOR = "sensor" +CONF_PUMP_OUT = "pump" +CONF_PUMP_CYCLE_TIME = "cycle_time" +CONF_PUMP_PROPORTIONAL_BAND = "proportional_band" +CONF_PUMP_VALUE = "on_pump_value" +CONF_CYCLE_START = "on_cycle_start" +CONF_DISABLE_CLOCK="disable_clock" +CONF_TARGET="target" + +def to_proportional_band(value): + try: + value = int(value) + except (TypeError, ValueError): + # pylint: disable=raise-missing-from + raise cv.Invalid(f"") + return cv.one_of(20, 50, 100, 150, 200, 250)(value) + +ChlorSensorOutputTrigger = chlorine_pump_ns.class_( + "ChlorSensorOutputTrigger", automation.Trigger.template(cg.bool_, cg.int_) +) +ChlorSensorCycleTrigger = chlorine_pump_ns.class_( + "ChlorSensorCycleTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + +PrimeAction = chlorine_pump_ns.class_("ChlorinePrime", automation.Action) +StartAction = chlorine_pump_ns.class_("ChlorineStart", automation.Action) +StopAction = chlorine_pump_ns.class_("ChlorineStop", automation.Action) +SetTargetAction = chlorine_pump_ns.class_("ChlorineSetTarget", automation.Action) + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ChlorinePump), + cv.Required(CONF_PUMP_OUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_SENSOR): cv.use_id(ChlorineSensor), + cv.Optional(CONF_PUMP_CYCLE_TIME, default=360): cv.int_range(min=30, max=1400), + cv.Optional(CONF_PUMP_PROPORTIONAL_BAND, default=200): to_proportional_band, + cv.Optional(CONF_DISABLE_CLOCK, default=False): cv.boolean, + cv.Optional(CONF_TARGET, 700): cv.int_range(300, 1400), + cv.Optional(CONF_PUMP_VALUE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChlorSensorOutputTrigger), + } + ), + cv.Optional(CONF_CYCLE_START): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChlorSensorCycleTrigger), + } + ), +}) + + + +ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(ChlorinePump) +}) + +@automation.register_action("chlorine_pump.prime", PrimeAction, ACTION_SCHEMA) +async def prime_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) + +@automation.register_action("chlorine_pump.start", StartAction, ACTION_SCHEMA) +async def start_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) + +@automation.register_action("chlorine_pump.stop", StopAction, ACTION_SCHEMA) +async def stop_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) + +@automation.register_action("chlorine_pump.set_target", SetTargetAction, ACTION_SCHEMA.extend({ + cv.Required(CONF_TARGET): cv.templatable(cv.float_), +})) +async def number_set_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_TARGET], args, float) + cg.add(var.set_value(template_)) + return var + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + #yield mqtt.register_mqtt_component(var, config) + + #btnReset = yield cg.gpio_pin_expression(config[CONF_SENSOR_PIN]) + #cg.add(var.set_input_pin(btnReset)) + + + sensor = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sensor)) + + pump_out = await cg.get_variable(config[CONF_PUMP_OUT]) + cg.add(var.set_pump_out(pump_out)) + + cg.add(var.set_cycle_time(config[CONF_PUMP_CYCLE_TIME])) # Needs be configured before sensor + + cg.add(var.disable_clock(config[CONF_DISABLE_CLOCK])) + #cycle_time = yield cg.get_variable(config[CONF_PUMP_CYCLE_TIME]) + + #prop_band = yield cg.get_variable(config[CONF_PUMP_PROPORTIONAL_BAND]) + cg.add(var.set_prop_band(config[CONF_PUMP_PROPORTIONAL_BAND])) + + #cg.add(var.set_state(config[CONF_STATE])) + + + if CONF_PUMP_VALUE in config: + for conf in config.get(CONF_PUMP_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "pState"), (int, "pTime")], conf) + + for conf in config.get(CONF_CYCLE_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(int, "tOn"), (int, "tOff")], conf) \ No newline at end of file diff --git a/external_components/chlorine_pump/pump_cycle.h b/external_components/chlorine_pump/pump_cycle.h new file mode 100644 index 0000000..98b96dc --- /dev/null +++ b/external_components/chlorine_pump/pump_cycle.h @@ -0,0 +1,216 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/gpio/output/gpio_binary_output.h" +#include "esphome/components/analog_orp/analog_orp.h" + +static const char *const TAG = "chlorine_pump"; + +namespace esphome { +namespace chlorine_pump { + +class ChlorinePump : public Component { + protected: + + analog_orp::ChlorineSensor *sensor_; + gpio::GPIOBinaryOutput *pump_out; + int cycle_time; + int prop_band; + bool state = true; + unsigned long last_action; + bool disable_clock_ = false; + CallbackManager callback_pump_; + CallbackManager callback_cycle_; + int tOn = 0; + int tOff = 0; + float target_ = 700.0f; + + public: + + ChlorinePump(){ + last_action = 0; + } + + + void disable_clock(bool disable_clock){ + this->disable_clock_ = disable_clock; + } + void set_sensor(analog_orp::ChlorineSensor *sensor){ + this->sensor_ = sensor; + } + void set_pump_out(gpio::GPIOBinaryOutput *pump_out){ + this->pump_out = pump_out; + } + void set_cycle_time(int time_in_sec){ + this->cycle_time = time_in_sec; + } + void set_prop_band(int prop_band){ + this->prop_band = prop_band; + } + void set_target(int target){ + this->target_ = (float) target; + } + + void setup() override { + last_action = millis(); + if(disable_clock_ && sensor_ != nullptr){ + if(!sensor_->has_averager()) sensor_->add_on_state_callback([=](float val) -> void { + this->tick_time(val); + }); + if(sensor_->has_averager()) sensor_->add_average_change_callback([=](float val) -> void { + this->tick_time(val); + }); + } + } + void prime() { + tOn = 30; + tOff = 2; + this->pump_out->turn_on(); + } + void start() { + this->state = true; + } + void stop() { + this->state = false; + tOn = 0; + tOff = 0; + this->pump_out->turn_off(); + } + bool get_state(){ + return state; + } + + + void setMillisPrecies(unsigned long waitPeriod){ + + if(last_action + waitPeriod + waitPeriod > millis()){ + last_action += waitPeriod; + } + else{ + ESP_LOGW(TAG, "Reset millis(). Did the system experience a halt for some reason?"); + last_action = millis(); + } + } + + int calculatePumpTimeSeconds(float last_mesurement){ + + if(last_mesurement >= target_) return 0; + + float currentVal = target_ - last_mesurement; + float proportionalValue = (currentVal / (float) prop_band); + float timeInSeconds = proportionalValue * (float) cycle_time; + + if(timeInSeconds > cycle_time) timeInSeconds = cycle_time; + + return (int) timeInSeconds; + } + + + + void add_on_pump_callback(std::function &&callback){ + this->callback_pump_.add(std::move(callback)); + } + void add_on_cycle_callback(std::function &&callback){ + this->callback_cycle_.add(std::move(callback)); + } + + void tick_time(float last_mesurement){ + + if(tOn == 0 && tOff == 0 && state){ + int seconds = calculatePumpTimeSeconds(last_mesurement); + + if(seconds == 0){ + tOn = 0; + tOff = 30; + } + + tOn = seconds; + tOff = cycle_time - tOn; + + ESP_LOGD(TAG, "Time => tOn: %d, tOff: %d", tOn, tOff); + + this->callback_cycle_.call(tOn, tOff); + } + else if(tOn > 0) { + this->callback_pump_.call(true, tOn); + --tOn; + pump_out->turn_on(); + } + else if(tOff > 0) { + this->callback_pump_.call(false, tOff); + --tOff; + pump_out->turn_off(); + } + } + + void loop() override{ + + if(disable_clock_) return; + + if(millis() - last_action > 1000){ + + tick_time(sensor_->get_state()); + + setMillisPrecies(1000); + } + } +}; + +class ChlorSensorOutputTrigger : public Trigger { + public: + explicit ChlorSensorOutputTrigger(ChlorinePump *parent) { + parent->add_on_pump_callback([this](bool output_state, int value) { this->trigger(output_state, value); }); + } +}; +class ChlorSensorCycleTrigger : public Trigger { + public: + explicit ChlorSensorCycleTrigger(ChlorinePump *parent) { + parent->add_on_cycle_callback([this](int on_time, int off_time) { this->trigger(on_time, off_time); }); + } +}; + +template class ChlorinePrime : public Action { + public: + ChlorinePrime(ChlorinePump *pump) : pump_(pump) {} + + void play(Ts... x) override { this->pump_->prime(); } + + protected: + ChlorinePump *pump_; +}; +template class ChlorineStart : public Action { + public: + ChlorineStart(ChlorinePump *pump) : pump_(pump) {} + + void play(Ts... x) override { this->pump_->start(); } + + protected: + ChlorinePump *pump_; +}; + +template class ChlorineStop : public Action { + public: + ChlorineStop(ChlorinePump *pump) : pump_(pump) {} + + void play(Ts... x) override { this->pump_->stop(); } + + protected: + ChlorinePump *pump_; +}; + +template class ChlorineSetTarget : public Action { + public: + ChlorineSetTarget(ChlorinePump *pump) : pump_(pump) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + this->pump_->set_target((int) this->value_.value(x...)); + } + + protected: + ChlorinePump *pump_; +}; + + +} +} \ No newline at end of file