diff --git a/CODEOWNERS b/CODEOWNERS index d3151c3f02..86e6cd978b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -79,6 +79,7 @@ esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb +esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py new file mode 100644 index 0000000000..e65fcf74ca --- /dev/null +++ b/esphome/components/ezo_pmp/__init__.py @@ -0,0 +1,291 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION +from esphome import automation +from esphome.automation import maybe_simple_id + +CODEOWNERS = ["@carlos-sarmiento"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_VOLUME = "volume" +CONF_VOLUME_PER_MINUTE = "volume_per_minute" + +ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp") +EzoPMP = ezo_pmp_ns.class_("EzoPMP", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EzoPMP), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(103)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +EZO_PMP_NO_ARGS_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + } +) + +# Actions that do not require more arguments + +EzoPMPFindAction = ezo_pmp_ns.class_("EzoPMPFindAction", automation.Action) +EzoPMPClearTotalVolumeDispensedAction = ezo_pmp_ns.class_( + "EzoPMPClearTotalVolumeDispensedAction", automation.Action +) +EzoPMPClearCalibrationAction = ezo_pmp_ns.class_( + "EzoPMPClearCalibrationAction", automation.Action +) +EzoPMPPauseDosingAction = ezo_pmp_ns.class_( + "EzoPMPPauseDosingAction", automation.Action +) +EzoPMPStopDosingAction = ezo_pmp_ns.class_("EzoPMPStopDosingAction", automation.Action) +EzoPMPDoseContinuouslyAction = ezo_pmp_ns.class_( + "EzoPMPDoseContinuouslyAction", automation.Action +) + +# Actions that require more arguments +EzoPMPDoseVolumeAction = ezo_pmp_ns.class_("EzoPMPDoseVolumeAction", automation.Action) +EzoPMPDoseVolumeOverTimeAction = ezo_pmp_ns.class_( + "EzoPMPDoseVolumeOverTimeAction", automation.Action +) +EzoPMPDoseWithConstantFlowRateAction = ezo_pmp_ns.class_( + "EzoPMPDoseWithConstantFlowRateAction", automation.Action +) +EzoPMPSetCalibrationVolumeAction = ezo_pmp_ns.class_( + "EzoPMPSetCalibrationVolumeAction", automation.Action +) +EzoPMPChangeI2CAddressAction = ezo_pmp_ns.class_( + "EzoPMPChangeI2CAddressAction", automation.Action +) +EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( + "EzoPMPArbitraryCommandAction", automation.Action +) + + +@automation.register_action( + "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_find_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( + "ezo_pmp.dose_continuously", + EzoPMPDoseContinuouslyAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_dose_continuously_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( + "ezo_pmp.clear_total_volume_dosed", + EzoPMPClearTotalVolumeDispensedAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_total_volume_dosed_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( + "ezo_pmp.clear_calibration", + EzoPMPClearCalibrationAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_calibration_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( + "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_pause_dosing_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( + "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_stop_dosing_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) + + +# Actions that require Multiple Args + +EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA +) +async def ezo_pmp_dose_volume_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_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume_over_time", + EzoPMPDoseVolumeOverTimeAction, + EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, +) +async def ezo_pmp_dose_volume_over_time_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_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME_PER_MINUTE): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_with_constant_flow_rate", + EzoPMPDoseWithConstantFlowRateAction, + EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, +) +async def ezo_pmp_dose_with_constant_flow_rate_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_VOLUME_PER_MINUTE], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.set_calibration_volume", + EzoPMPSetCalibrationVolumeAction, + EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, +) +async def ezo_pmp_set_calibration_volume_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_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_ADDRESS): cv.templatable(cv.int_range(min=1, max=127)), + } +) + + +@automation.register_action( + "ezo_pmp.change_i2c_address", + EzoPMPChangeI2CAddressAction, + EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, +) +async def ezo_pmp_change_i2c_address_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_ADDRESS], args, cg.double) + cg.add(var.set_address(template_)) + + return var + + +EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_COMMAND): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "ezo_pmp.arbitrary_command", + EzoPMPArbitraryCommandAction, + EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, +) +async def ezo_pmp_arbitrary_command_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_COMMAND], args, cg.std_string) + cg.add(var.set_command(template_)) + + return var diff --git a/esphome/components/ezo_pmp/binary_sensor.py b/esphome/components/ezo_pmp/binary_sensor.py new file mode 100644 index 0000000000..582eb7af25 --- /dev/null +++ b/esphome/components/ezo_pmp/binary_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_RUNNING, + DEVICE_CLASS_EMPTY, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_PUMP_STATE = "pump_state" +CONF_IS_PAUSED = "is_paused" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_PUMP_STATE): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_IS_PAUSED): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_EMPTY, + entity_category=ENTITY_CATEGORY_NONE, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_PUMP_STATE in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_PUMP_STATE]) + cg.add(parent.set_is_dosing(sens)) + + if CONF_IS_PAUSED in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_IS_PAUSED]) + cg.add(parent.set_is_paused(sens)) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp new file mode 100644 index 0000000000..9c96ce2af0 --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -0,0 +1,542 @@ +#include "ezo_pmp.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ezo_pmp { + +static const char *const TAG = "ezo-pmp"; + +static const uint16_t EZO_PMP_COMMAND_NONE = 0; +static const uint16_t EZO_PMP_COMMAND_TYPE_READ = 1; + +static const uint16_t EZO_PMP_COMMAND_FIND = 2; +static const uint16_t EZO_PMP_COMMAND_DOSE_CONTINUOUSLY = 4; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME = 8; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME = 16; +static const uint16_t EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE = 32; +static const uint16_t EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME = 64; +static const uint16_t EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED = 128; +static const uint16_t EZO_PMP_COMMAND_CLEAR_CALIBRATION = 256; +static const uint16_t EZO_PMP_COMMAND_PAUSE_DOSING = 512; +static const uint16_t EZO_PMP_COMMAND_STOP_DOSING = 1024; +static const uint16_t EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS = 2048; +static const uint16_t EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS = 4096; + +static const uint16_t EZO_PMP_COMMAND_READ_DOSING = 3; +static const uint16_t EZO_PMP_COMMAND_READ_SINGLE_REPORT = 5; +static const uint16_t EZO_PMP_COMMAND_READ_MAX_FLOW_RATE = 9; +static const uint16_t EZO_PMP_COMMAND_READ_PAUSE_STATUS = 17; +static const uint16_t EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED = 33; +static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65; +static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129; +static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257; + +static const std::string DOSING_MODE_NONE = "None"; +static const std::string DOSING_MODE_VOLUME = "Volume"; +static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time"; +static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate"; +static const std::string DOSING_MODE_CONTINUOUS = "Continuous"; + +void EzoPMP::dump_config() { + LOG_I2C_DEVICE(this); + if (this->is_failed()) + ESP_LOGE(TAG, "Communication with EZO-PMP circuit failed!"); + LOG_UPDATE_INTERVAL(this); +} + +void EzoPMP::update() { + if (this->is_waiting_) { + return; + } + + if (this->is_first_read_) { + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); + this->is_first_read_ = false; + } + + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + + if (this->is_dosing_flag_) { + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + } + + this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_); + } else { + ESP_LOGV(TAG, "Not Scheduling new Command during update()"); + } +} + +void EzoPMP::loop() { + // If we are not waiting for anything and there is no command to be sent, return + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + return; + } + + // If we are not waiting for anything and there IS a command to be sent, do it. + if (!this->is_waiting_ && this->peek_next_command_() != EZO_PMP_COMMAND_NONE) { + this->send_next_command_(); + } + + // If we are waiting for something but it isn't ready yet, then return + if (this->is_waiting_ && millis() - this->start_time_ < this->wait_time_) { + return; + } + + // We are waiting for something and it should be ready. + this->read_command_result_(); +} + +void EzoPMP::clear_current_command_() { + this->current_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = false; +} + +void EzoPMP::read_command_result_() { + uint8_t response_buffer[21] = {'\0'}; + + response_buffer[0] = 0; + if (!this->read_bytes_raw(response_buffer, 20)) { + ESP_LOGE(TAG, "read error"); + this->clear_current_command_(); + return; + } + + switch (response_buffer[0]) { + case 254: + return; // keep waiting + case 1: + break; + case 2: + ESP_LOGE(TAG, "device returned a syntax error"); + this->clear_current_command_(); + return; + case 255: + ESP_LOGE(TAG, "device returned no data"); + this->clear_current_command_(); + return; + default: + ESP_LOGE(TAG, "device returned an unknown response: %d", response_buffer[0]); + this->clear_current_command_(); + return; + } + + char first_parameter_buffer[10] = {'\0'}; + char second_parameter_buffer[10] = {'\0'}; + char third_parameter_buffer[10] = {'\0'}; + + first_parameter_buffer[0] = '\0'; + second_parameter_buffer[0] = '\0'; + third_parameter_buffer[0] = '\0'; + + int current_parameter = 1; + + size_t position_in_parameter_buffer = 0; + // some sensors return multiple comma-separated values, terminate string after first one + for (size_t i = 1; i < sizeof(response_buffer) - 1; i++) { + char current_char = response_buffer[i]; + + if (current_char == '\0') { + ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer); + ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer); + ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer); + ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer); + + break; + } + + if (current_char == ',') { + current_parameter++; + position_in_parameter_buffer = 0; + continue; + } + + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + + position_in_parameter_buffer++; + } + + auto parsed_first_parameter = parse_number(first_parameter_buffer); + auto parsed_second_parameter = parse_number(second_parameter_buffer); + auto parsed_third_parameter = parse_number(third_parameter_buffer); + + switch (this->current_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + if (parsed_third_parameter.has_value()) + this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1; + + if (this->is_dosing_) + this->is_dosing_->publish_state(this->is_dosing_flag_); + + if (parsed_second_parameter.has_value() && this->last_volume_requested_) { + this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0)); + } + + if (!this->is_dosing_flag_ && !this->is_paused_flag_) { + // If pump is not paused and not dispensing + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + } + + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) { + this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0)); + } + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57) + if (parsed_second_parameter.has_value() && this->max_flow_rate_) + this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61) + if (parsed_second_parameter.has_value()) + this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1; + + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->total_volume_dosed_) + this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_) + this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65) + if (parsed_second_parameter.has_value() && this->calibration_status_) { + if (parsed_second_parameter.value_or(0) == 1) { + this->calibration_status_->publish_state("Fixed Volume"); + } else if (parsed_second_parameter.value_or(0) == 2) { + this->calibration_status_->publish_state("Volume/Time"); + } else if (parsed_second_parameter.value_or(0) == 3) { + this->calibration_status_->publish_state("Fixed Volume & Volume/Time"); + } else { + this->calibration_status_->publish_state("Uncalibrated"); + } + } + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67) + if (parsed_second_parameter.has_value() && this->pump_voltage_) + this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0)); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE) + this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE); + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS) + this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + this->is_paused_flag_ = false; + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: + ESP_LOGI(TAG, "Arbitrary Command Response: %s", (char *) response_buffer); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + case EZO_PMP_COMMAND_FIND: // Find (page 52) + // Nothing to do here + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->current_command_); + return; + } + + this->clear_current_command_(); +} + +void EzoPMP::send_next_command_() { + int wait_time_for_command = 400; // milliseconds + uint8_t command_buffer[21]; + int command_buffer_length = 0; + + this->pop_next_command_(); // this->next_command will be updated. + + switch (this->next_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + command_buffer_length = sprintf((char *) command_buffer, "D,?"); + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + command_buffer_length = sprintf((char *) command_buffer, "R"); + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: + command_buffer_length = sprintf((char *) command_buffer, "DC,?"); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "P,?"); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "TV,?"); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "ATV,?"); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "Cal,?"); + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: + command_buffer_length = sprintf((char *) command_buffer, "PV,?"); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_FIND: // Find (page 52) + command_buffer_length = sprintf((char *) command_buffer, "Find"); + wait_time_for_command = 60000; // This command will block all updates for a minute + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + command_buffer_length = sprintf((char *) command_buffer, "D,*"); + break; + + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + command_buffer_length = sprintf((char *) command_buffer, "Clear"); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,clear"); + break; + + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + command_buffer_length = sprintf((char *) command_buffer, "P"); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + command_buffer_length = sprintf((char *) command_buffer, "X"); + break; + + // Non-Read commands with parameters + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + command_buffer_length = + sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + command_buffer_length = + sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) + command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command + command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_); + ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->next_command_); + return; + } + + // Send command + ESP_LOGV(TAG, "Sending command to device: %s", (char *) command_buffer); + this->write(command_buffer, command_buffer_length); + + this->current_command_ = this->next_command_; + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = true; + this->start_time_ = millis(); + this->wait_time_ = wait_time_for_command; +} + +void EzoPMP::pop_next_command_() { + if (this->next_command_queue_length_ <= 0) { + ESP_LOGE(TAG, "Tried to dequeue command from empty queue"); + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->next_command_volume_ = 0; + this->next_command_duration_ = 0; + return; + } + + // Read from Head + this->next_command_ = this->next_command_queue_[this->next_command_queue_head_]; + this->next_command_volume_ = this->next_command_volume_queue_[this->next_command_queue_head_]; + this->next_command_duration_ = this->next_command_duration_queue_[this->next_command_queue_head_]; + + // Move positions + next_command_queue_head_++; + if (next_command_queue_head_ >= 10) { + next_command_queue_head_ = 0; + } + + next_command_queue_length_--; +} + +uint16_t EzoPMP::peek_next_command_() { + if (this->next_command_queue_length_ <= 0) { + return EZO_PMP_COMMAND_NONE; + } + + return this->next_command_queue_[this->next_command_queue_head_]; +} + +void EzoPMP::queue_command_(uint16_t command, double volume, int duration, bool should_schedule) { + if (!should_schedule) { + return; + } + + if (this->next_command_queue_length_ >= 10) { + ESP_LOGE(TAG, "Tried to queue command '%d' but queue is full", command); + return; + } + + this->next_command_queue_[this->next_command_queue_last_] = command; + this->next_command_volume_queue_[this->next_command_queue_last_] = volume; + this->next_command_duration_queue_[this->next_command_queue_last_] = duration; + + ESP_LOGV(TAG, "Queue command '%d' in position '%d'", command, next_command_queue_last_); + + // Move positions + next_command_queue_last_++; + if (next_command_queue_last_ >= 10) { + next_command_queue_last_ = 0; + } + + next_command_queue_length_++; +} + +// Actions + +void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); } + +void EzoPMP::dose_continuously() { + this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume_over_time(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::set_calibration_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::clear_total_volume_dosed() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, true); +} + +void EzoPMP::clear_calibration() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_CALIBRATION, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::pause_dosing() { + this->queue_command_(EZO_PMP_COMMAND_PAUSE_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); +} + +void EzoPMP::stop_dosing() { this->queue_command_(EZO_PMP_COMMAND_STOP_DOSING, 0, 0, true); } + +void EzoPMP::change_i2c_address(int address) { + this->queue_command_(EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS, 0, address, true); +} + +void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { + this->arbitrary_command_ = command.c_str(); + this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); +} + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h new file mode 100644 index 0000000000..b41710cd78 --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -0,0 +1,252 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/i2c/i2c.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +namespace esphome { +namespace ezo_pmp { + +class EzoPMP : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void loop() override; + void update() override; + +#ifdef USE_SENSOR + void set_current_volume_dosed(sensor::Sensor *current_volume_dosed) { current_volume_dosed_ = current_volume_dosed; } + void set_total_volume_dosed(sensor::Sensor *total_volume_dosed) { total_volume_dosed_ = total_volume_dosed; } + void set_absolute_total_volume_dosed(sensor::Sensor *absolute_total_volume_dosed) { + absolute_total_volume_dosed_ = absolute_total_volume_dosed; + } + void set_pump_voltage(sensor::Sensor *pump_voltage) { pump_voltage_ = pump_voltage; } + void set_last_volume_requested(sensor::Sensor *last_volume_requested) { + last_volume_requested_ = last_volume_requested; + } + void set_max_flow_rate(sensor::Sensor *max_flow_rate) { max_flow_rate_ = max_flow_rate; } +#endif + +#ifdef USE_BINARY_SENSOR + void set_is_dosing(binary_sensor::BinarySensor *is_dosing) { is_dosing_ = is_dosing; } + void set_is_paused(binary_sensor::BinarySensor *is_paused) { is_paused_ = is_paused; } +#endif + +#ifdef USE_TEXT_SENSOR + void set_dosing_mode(text_sensor::TextSensor *dosing_mode) { dosing_mode_ = dosing_mode; } + void set_calibration_status(text_sensor::TextSensor *calibration_status) { calibration_status_ = calibration_status; } +#endif + + // Actions for EZO-PMP + void find(); + void dose_continuously(); + void dose_volume(double volume); + void dose_volume_over_time(double volume, int duration); + void dose_with_constant_flow_rate(double volume, int duration); + void set_calibration_volume(double volume); + void clear_total_volume_dosed(); + void clear_calibration(); + void pause_dosing(); + void stop_dosing(); + void change_i2c_address(int address); + void exec_arbitrary_command(const std::basic_string &command); + + protected: + uint32_t start_time_ = 0; + uint32_t wait_time_ = 0; + bool is_waiting_ = false; + bool is_first_read_ = true; + + uint16_t next_command_ = 0; + double next_command_volume_ = 0; // might be negative + int next_command_duration_ = 0; + + uint16_t next_command_queue_[10]; + double next_command_volume_queue_[10]; + int next_command_duration_queue_[10]; + int next_command_queue_head_ = 0; + int next_command_queue_last_ = 0; + int next_command_queue_length_ = 0; + + uint16_t current_command_ = 0; + bool is_paused_flag_ = false; + bool is_dosing_flag_ = false; + + const char *arbitrary_command_{nullptr}; + + void send_next_command_(); + void read_command_result_(); + void clear_current_command_(); + void queue_command_(uint16_t command, double volume, int duration, bool should_schedule); + void pop_next_command_(); + uint16_t peek_next_command_(); + +#ifdef USE_SENSOR + sensor::Sensor *current_volume_dosed_{nullptr}; + sensor::Sensor *total_volume_dosed_{nullptr}; + sensor::Sensor *absolute_total_volume_dosed_{nullptr}; + sensor::Sensor *pump_voltage_{nullptr}; + sensor::Sensor *max_flow_rate_{nullptr}; + sensor::Sensor *last_volume_requested_{nullptr}; +#endif + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *is_dosing_{nullptr}; + binary_sensor::BinarySensor *is_paused_{nullptr}; +#endif + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *dosing_mode_{nullptr}; + text_sensor::TextSensor *calibration_status_{nullptr}; +#endif +}; + +// Action Templates +template class EzoPMPFindAction : public Action { + public: + EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->find(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseContinuouslyAction : public Action { + public: + EzoPMPDoseContinuouslyAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_continuously(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeAction : public Action { + public: + EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeOverTimeAction : public Action { + public: + EzoPMPDoseVolumeOverTimeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseWithConstantFlowRateAction : public Action { + public: + EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPSetCalibrationVolumeAction : public Action { + public: + EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearTotalVolumeDispensedAction : public Action { + public: + EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearCalibrationAction : public Action { + public: + EzoPMPClearCalibrationAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_calibration(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPPauseDosingAction : public Action { + public: + EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPStopDosingAction : public Action { + public: + EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPChangeI2CAddressAction : public Action { + public: + EzoPMPChangeI2CAddressAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } + TEMPLATABLE_VALUE(int, address) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPArbitraryCommandAction : public Action { + public: + EzoPMPArbitraryCommandAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + TEMPLATABLE_VALUE(std::string, command) + + protected: + EzoPMP *ezopmp_; +}; + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/sensor.py b/esphome/components/ezo_pmp/sensor.py new file mode 100644 index 0000000000..737985f4c5 --- /dev/null +++ b/esphome/components/ezo_pmp/sensor.py @@ -0,0 +1,104 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + CONF_ID, + UNIT_VOLT, +) + +from . import EzoPMP + + +DEPENDENCIES = ["ezo_pmp"] + +CONF_CURRENT_VOLUME_DOSED = "current_volume_dosed" +CONF_TOTAL_VOLUME_DOSED = "total_volume_dosed" +CONF_ABSOLUTE_TOTAL_VOLUME_DOSED = "absolute_total_volume_dosed" +CONF_PUMP_VOLTAGE = "pump_voltage" +CONF_LAST_VOLUME_REQUESTED = "last_volume_requested" +CONF_MAX_FLOW_RATE = "max_flow_rate" + +UNIT_MILILITER = "ml" +UNIT_MILILITERS_PER_MINUTE = "ml/min" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_CURRENT_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_LAST_VOLUME_REQUESTED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_MAX_FLOW_RATE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITERS_PER_MINUTE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ABSOLUTE_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_PUMP_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_CURRENT_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_CURRENT_VOLUME_DOSED]) + cg.add(parent.set_current_volume_dosed(sens)) + + if CONF_LAST_VOLUME_REQUESTED in config: + sens = await sensor.new_sensor(config[CONF_LAST_VOLUME_REQUESTED]) + cg.add(parent.set_last_volume_requested(sens)) + + if CONF_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_total_volume_dosed(sens)) + + if CONF_ABSOLUTE_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_ABSOLUTE_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_absolute_total_volume_dosed(sens)) + + if CONF_PUMP_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_PUMP_VOLTAGE]) + cg.add(parent.set_pump_voltage(sens)) + + if CONF_MAX_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_MAX_FLOW_RATE]) + cg.add(parent.set_max_flow_rate(sens)) diff --git a/esphome/components/ezo_pmp/text_sensor.py b/esphome/components/ezo_pmp/text_sensor.py new file mode 100644 index 0000000000..f8f133e316 --- /dev/null +++ b/esphome/components/ezo_pmp/text_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_DOSING_MODE = "dosing_mode" +CONF_CALIBRATION_STATUS = "calibration_status" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_DOSING_MODE): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_CALIBRATION_STATUS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_DOSING_MODE in config: + sens = await text_sensor.new_text_sensor(config[CONF_DOSING_MODE]) + cg.add(parent.set_dosing_mode(sens)) + + if CONF_CALIBRATION_STATUS in config: + sens = await text_sensor.new_text_sensor(config[CONF_CALIBRATION_STATUS]) + cg.add(parent.set_calibration_status(sens)) diff --git a/tests/test5.yaml b/tests/test5.yaml index 7fc20c452f..131d2f5b7f 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -165,6 +165,12 @@ binary_sensor: + - platform: ezo_pmp + pump_state: + name: "Pump State" + is_paused: + name: "Is Paused" + tlc5947: data_pin: GPIO12 clock_pin: GPIO14 @@ -220,6 +226,10 @@ esp32_improv: authorized_duration: 1min status_indicator: built_in_led +ezo_pmp: + id: hcl_pump + update_interval: 1s + number: - platform: template name: My template number @@ -440,6 +450,20 @@ sensor: cold_junction: name: Ambient Temperature + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + script: - id: automation_test then: @@ -487,3 +511,33 @@ display: lambda: |- it.print("81818181"); +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,?