diff --git a/CODEOWNERS b/CODEOWNERS index 714ef58538..605222df63 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -87,6 +87,7 @@ esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter +esphome/components/pipsolar/* @andreashergert1984 esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py new file mode 100644 index 0000000000..20e4672125 --- /dev/null +++ b/esphome/components/pipsolar/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.components import uart + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@andreashergert1984"] +AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "output"] +MULTI_CONF = True + +CONF_PIPSOLAR_ID = "pipsolar_id" + +pipsolar_ns = cg.esphome_ns.namespace("pipsolar") +PipsolarComponent = pipsolar_ns.class_("Pipsolar", cg.Component) + +PIPSOLAR_COMPONENT_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_PIPSOLAR_ID): cv.use_id(PipsolarComponent), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PipsolarComponent)}) + .extend(cv.polling_component_schema("1s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py new file mode 100644 index 0000000000..5c6af3bffc --- /dev/null +++ b/esphome/components/pipsolar/binary_sensor/__init__.py @@ -0,0 +1,144 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_LOAD_STATUS = "load_status" +CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING = ( + "battery_voltage_to_steady_while_charging" +) +CONF_CHARGING_STATUS = "charging_status" +CONF_SCC_CHARGING_STATUS = "scc_charging_status" +CONF_AC_CHARGING_STATUS = "ac_charging_status" +CONF_CHARGING_TO_FLOATING_MODE = "charging_to_floating_mode" +CONF_SWITCH_ON = "switch_on" +CONF_DUSTPROOF_INSTALLED = "dustproof_installed" +CONF_SILENCE_BUZZER_OPEN_BUZZER = "silence_buzzer_open_buzzer" +CONF_OVERLOAD_BYPASS_FUNCTION = "overload_bypass_function" +CONF_LCD_ESCAPE_TO_DEFAULT = "lcd_escape_to_default" +CONF_OVERLOAD_RESTART_FUNCTION = "overload_restart_function" +CONF_OVER_TEMPERATURE_RESTART_FUNCTION = "over_temperature_restart_function" +CONF_BACKLIGHT_ON = "backlight_on" +CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT = "alarm_on_when_primary_source_interrupt" +CONF_FAULT_CODE_RECORD = "fault_code_record" +CONF_POWER_SAVING = "power_saving" + +CONF_WARNINGS_PRESENT = "warnings_present" +CONF_FAULTS_PRESENT = "faults_present" +CONF_WARNING_POWER_LOSS = "warning_power_loss" +CONF_FAULT_INVERTER_FAULT = "fault_inverter_fault" +CONF_FAULT_BUS_OVER = "fault_bus_over" +CONF_FAULT_BUS_UNDER = "fault_bus_under" +CONF_FAULT_BUS_SOFT_FAIL = "fault_bus_soft_fail" +CONF_WARNING_LINE_FAIL = "warning_line_fail" +CONF_FAULT_OPVSHORT = "fault_opvshort" +CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW = "fault_inverter_voltage_too_low" +CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH = "fault_inverter_voltage_too_high" +CONF_WARNING_OVER_TEMPERATURE = "warning_over_temperature" +CONF_WARNING_FAN_LOCK = "warning_fan_lock" +CONF_WARNING_BATTERY_VOLTAGE_HIGH = "warning_battery_voltage_high" +CONF_WARNING_BATTERY_LOW_ALARM = "warning_battery_low_alarm" +CONF_WARNING_BATTERY_UNDER_SHUTDOWN = "warning_battery_under_shutdown" +CONF_WARNING_BATTERY_DERATING = "warning_battery_derating" +CONF_WARNING_OVER_LOAD = "warning_over_load" +CONF_WARNING_EEPROM_FAILED = "warning_eeprom_failed" +CONF_FAULT_INVERTER_OVER_CURRENT = "fault_inverter_over_current" +CONF_FAULT_INVERTER_SOFT_FAILED = "fault_inverter_soft_failed" +CONF_FAULT_SELF_TEST_FAILED = "fault_self_test_failed" +CONF_FAULT_OP_DC_VOLTAGE_OVER = "fault_op_dc_voltage_over" +CONF_FAULT_BATTERY_OPEN = "fault_battery_open" +CONF_FAULT_CURRENT_SENSOR_FAILED = "fault_current_sensor_failed" +CONF_FAULT_BATTERY_SHORT = "fault_battery_short" +CONF_WARNING_POWER_LIMIT = "warning_power_limit" +CONF_WARNING_PV_VOLTAGE_HIGH = "warning_pv_voltage_high" +CONF_FAULT_MPPT_OVERLOAD = "fault_mppt_overload" +CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload" +CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge" +CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current" +CONF_FAULT_CODE = "fault_code" +CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy" +CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = ( + "warning_high_ac_input_during_bus_soft_start" +) +CONF_WARNING_BATTERY_EQUALIZATION = "warning_battery_equalization" + +TYPES = [ + CONF_ADD_SBU_PRIORITY_VERSION, + CONF_CONFIGURATION_STATUS, + CONF_SCC_FIRMWARE_VERSION, + CONF_LOAD_STATUS, + CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING, + CONF_CHARGING_STATUS, + CONF_SCC_CHARGING_STATUS, + CONF_AC_CHARGING_STATUS, + CONF_CHARGING_TO_FLOATING_MODE, + CONF_SWITCH_ON, + CONF_DUSTPROOF_INSTALLED, + CONF_SILENCE_BUZZER_OPEN_BUZZER, + CONF_OVERLOAD_BYPASS_FUNCTION, + CONF_LCD_ESCAPE_TO_DEFAULT, + CONF_OVERLOAD_RESTART_FUNCTION, + CONF_OVER_TEMPERATURE_RESTART_FUNCTION, + CONF_BACKLIGHT_ON, + CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT, + CONF_FAULT_CODE_RECORD, + CONF_POWER_SAVING, + CONF_WARNINGS_PRESENT, + CONF_FAULTS_PRESENT, + CONF_WARNING_POWER_LOSS, + CONF_FAULT_INVERTER_FAULT, + CONF_FAULT_BUS_OVER, + CONF_FAULT_BUS_UNDER, + CONF_FAULT_BUS_SOFT_FAIL, + CONF_WARNING_LINE_FAIL, + CONF_FAULT_OPVSHORT, + CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW, + CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH, + CONF_WARNING_OVER_TEMPERATURE, + CONF_WARNING_FAN_LOCK, + CONF_WARNING_BATTERY_VOLTAGE_HIGH, + CONF_WARNING_BATTERY_LOW_ALARM, + CONF_WARNING_BATTERY_UNDER_SHUTDOWN, + CONF_WARNING_BATTERY_DERATING, + CONF_WARNING_OVER_LOAD, + CONF_WARNING_EEPROM_FAILED, + CONF_FAULT_INVERTER_OVER_CURRENT, + CONF_FAULT_INVERTER_SOFT_FAILED, + CONF_FAULT_SELF_TEST_FAILED, + CONF_FAULT_OP_DC_VOLTAGE_OVER, + CONF_FAULT_BATTERY_OPEN, + CONF_FAULT_CURRENT_SENSOR_FAILED, + CONF_FAULT_BATTERY_SHORT, + CONF_WARNING_POWER_LIMIT, + CONF_WARNING_PV_VOLTAGE_HIGH, + CONF_FAULT_MPPT_OVERLOAD, + CONF_WARNING_MPPT_OVERLOAD, + CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE, + CONF_FAULT_DC_DC_OVER_CURRENT, + CONF_FAULT_CODE, + CONF_WARNUNG_LOW_PV_ENERGY, + CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, + CONF_WARNING_BATTERY_EQUALIZATION, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): binary_sensor.BINARY_SENSOR_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + for type in TYPES: + if type in config: + conf = config[type] + sens = cg.new_Pvariable(conf[CONF_ID]) + await binary_sensor.register_binary_sensor(sens, conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py new file mode 100644 index 0000000000..b518d485e7 --- /dev/null +++ b/esphome/components/pipsolar/output/__init__.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import output +from esphome.const import CONF_ID, CONF_VALUE +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID, pipsolar_ns + +DEPENDENCIES = ["pipsolar"] + +PipsolarOutput = pipsolar_ns.class_("PipsolarOutput", output.FloatOutput) +SetOutputAction = pipsolar_ns.class_("SetOutputAction", automation.Action) + +CONF_POSSIBLE_VALUES = "possible_values" + +# 3.11 PCVV: Setting battery C.V. (constant voltage) charging voltage 48.0V ~ 58.4V for 48V unit +# battery_bulk_voltage; +# battery_recharge_voltage; 12V unit: 11V/11.3V/11.5V/11.8V/12V/12.3V/12.5V/12.8V +# 24V unit: 22V/22.5V/23V/23.5V/24V/24.5V/25V/25.5V +# 48V unit: 44V/45V/46V/47V/48V/49V/50V/51V +# battery_under_voltage; 40.0V ~ 48.0V for 48V unit +# battery_float_voltage; 48.0V ~ 58.4V for 48V unit +# battery_type; 00 for AGM, 01 for Flooded battery +# current_max_ac_charging_current; +# output_source_priority; 00 / 01 / 02 +# charger_source_priority; For HS: 00 for utility first, 01 for solar first, 02 for solar and utility, 03 for only solar charging +# For MS/MSX: 00 for utility first, 01 for solar first, 03 for only solar charging +# battery_redischarge_voltage; 12V unit: 00.0V12V/12.3V/12.5V/12.8V/13V/13.3V/13.5V/13.8V/14V/14.3V/14.5 +# 24V unit: 00.0V/24V/24.5V/25V/25.5V/26V/26.5V/27V/27.5V/28V/28.5V/29V +# 48V unit: 00.0V48V/49V/50V/51V/52V/53V/54V/55V/56V/57V/58V + +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" + +TYPES = { + CONF_BATTERY_RECHARGE_VOLTAGE: ( + [44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0], + "PBCV%02.1f", + ), + CONF_BATTERY_UNDER_VOLTAGE: ( + [40.0, 40.1, 42, 43, 44, 45, 46, 47, 48.0], + "PSDV%02.1f", + ), + CONF_BATTERY_FLOAT_VOLTAGE: ([48.0, 49.0, 50.0, 51.0], "PBFT%02.1f"), + CONF_BATTERY_TYPE: ([0, 1, 2], "PBT%02.0f"), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: ([2, 10, 20], "MUCHGC0%02.0f"), + CONF_CURRENT_MAX_CHARGING_CURRENT: ([10, 20, 30, 40], "MCHGC0%02.0f"), + CONF_OUTPUT_SOURCE_PRIORITY: ([0, 1, 2], "POP%02.0f"), + CONF_CHARGER_SOURCE_PRIORITY: ([0, 1, 2, 3], "PCP%02.0f"), + CONF_BATTERY_REDISCHARGE_VOLTAGE: ( + [0, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58], + "PBDV%02.1f", + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(PipsolarOutput), + cv.Optional(CONF_POSSIBLE_VALUES, default=values): cv.All( + cv.ensure_list(cv.positive_float), cv.Length(min=1) + ), + } + ) + for type, (values, _) in TYPES.items() + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (_, command) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await output.register_output(var, conf) + cg.add(var.set_parent(paren)) + cg.add(var.set_set_command(command)) + if (CONF_POSSIBLE_VALUES) in conf: + cg.add(var.set_possible_values(conf[CONF_POSSIBLE_VALUES])) + + +@automation.register_action( + "output.pipsolar.set_level", + SetOutputAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(CONF_ID), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), + } + ), +) +def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_level(template_)) + yield var diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp new file mode 100644 index 0000000000..b843f1f3e6 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -0,0 +1,22 @@ +#include "pipsolar_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.output"; + +void PipsolarOutput::write_state(float state) { + char tmp[10]; + sprintf(tmp, this->set_command_.c_str(), state); + + if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) { + ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state); + this->parent_->switch_command(std::string(tmp)); + } else { + ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); + } +} +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h new file mode 100644 index 0000000000..932efe01c2 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -0,0 +1,40 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +class Pipsolar; + +class PipsolarOutput : public output::FloatOutput { + public: + PipsolarOutput() {} + void set_parent(Pipsolar *parent) { this->parent_ = parent; } + void set_set_command(std::string command) { this->set_command_ = std::move(command); }; + void set_possible_values(std::vector possible_values) { this->possible_values_ = std::move(possible_values); } + void set_value(float value) { this->write_state(value); }; + + protected: + void write_state(float state) override; + std::string set_command_; + Pipsolar *parent_; + std::vector possible_values_; +}; + +template class SetOutputAction : public Action { + public: + SetOutputAction(PipsolarOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, level) + + void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + + protected: + PipsolarOutput *output_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp new file mode 100644 index 0000000000..9c7adc13c1 --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -0,0 +1,922 @@ +#include "pipsolar.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar"; + +void Pipsolar::setup() { + this->state_ = STATE_IDLE; + this->command_start_millis_ = 0; +} + +void Pipsolar::empty_uart_buffer_() { + uint8_t byte; + while (this->available()) { + this->read_byte(&byte); + } +} + +void Pipsolar::loop() { + // Read message + if (this->state_ == STATE_IDLE) { + this->empty_uart_buffer_(); + switch (this->send_next_command_()) { + case 0: + // no command send (empty queue) time to poll + if (millis() - this->last_poll_ > this->update_interval_) { + this->send_next_poll_(); + this->last_poll_ = millis(); + } + return; + break; + case 1: + // command send + return; + break; + } + } + if (this->state_ == STATE_COMMAND_COMPLETE) { + if (this->check_incoming_length_(4)) { + ESP_LOGD(TAG, "response length for command OK"); + if (this->check_incoming_crc_()) { + // crc ok + if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { + ESP_LOGD(TAG, "command successful"); + } else { + ESP_LOGD(TAG, "command not successful"); + } + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + + } else { + // crc failed + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } else { + ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", + this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_POLL_DECODED) { + std::string mode; + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + if (this->grid_rating_voltage_) { + this->grid_rating_voltage_->publish_state(value_grid_rating_voltage_); + } + if (this->grid_rating_current_) { + this->grid_rating_current_->publish_state(value_grid_rating_current_); + } + if (this->ac_output_rating_voltage_) { + this->ac_output_rating_voltage_->publish_state(value_ac_output_rating_voltage_); + } + if (this->ac_output_rating_frequency_) { + this->ac_output_rating_frequency_->publish_state(value_ac_output_rating_frequency_); + } + if (this->ac_output_rating_current_) { + this->ac_output_rating_current_->publish_state(value_ac_output_rating_current_); + } + if (this->ac_output_rating_apparent_power_) { + this->ac_output_rating_apparent_power_->publish_state(value_ac_output_rating_apparent_power_); + } + if (this->ac_output_rating_active_power_) { + this->ac_output_rating_active_power_->publish_state(value_ac_output_rating_active_power_); + } + if (this->battery_rating_voltage_) { + this->battery_rating_voltage_->publish_state(value_battery_rating_voltage_); + } + if (this->battery_recharge_voltage_) { + this->battery_recharge_voltage_->publish_state(value_battery_recharge_voltage_); + } + if (this->battery_under_voltage_) { + this->battery_under_voltage_->publish_state(value_battery_under_voltage_); + } + if (this->battery_bulk_voltage_) { + this->battery_bulk_voltage_->publish_state(value_battery_bulk_voltage_); + } + if (this->battery_float_voltage_) { + this->battery_float_voltage_->publish_state(value_battery_float_voltage_); + } + if (this->battery_type_) { + this->battery_type_->publish_state(value_battery_type_); + } + if (this->current_max_ac_charging_current_) { + this->current_max_ac_charging_current_->publish_state(value_current_max_ac_charging_current_); + } + if (this->current_max_charging_current_) { + this->current_max_charging_current_->publish_state(value_current_max_charging_current_); + } + if (this->input_voltage_range_) { + this->input_voltage_range_->publish_state(value_input_voltage_range_); + } + // special for input voltage range switch + if (this->input_voltage_range_switch_) { + this->input_voltage_range_switch_->publish_state(value_input_voltage_range_ == 1); + } + if (this->output_source_priority_) { + this->output_source_priority_->publish_state(value_output_source_priority_); + } + // special for output source priority switches + if (this->output_source_priority_utility_switch_) { + this->output_source_priority_utility_switch_->publish_state(value_output_source_priority_ == 0); + } + if (this->output_source_priority_solar_switch_) { + this->output_source_priority_solar_switch_->publish_state(value_output_source_priority_ == 1); + } + if (this->output_source_priority_battery_switch_) { + this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); + } + if (this->charger_source_priority_) { + this->charger_source_priority_->publish_state(value_charger_source_priority_); + } + if (this->parallel_max_num_) { + this->parallel_max_num_->publish_state(value_parallel_max_num_); + } + if (this->machine_type_) { + this->machine_type_->publish_state(value_machine_type_); + } + if (this->topology_) { + this->topology_->publish_state(value_topology_); + } + if (this->output_mode_) { + this->output_mode_->publish_state(value_output_mode_); + } + if (this->battery_redischarge_voltage_) { + this->battery_redischarge_voltage_->publish_state(value_battery_redischarge_voltage_); + } + if (this->pv_ok_condition_for_parallel_) { + this->pv_ok_condition_for_parallel_->publish_state(value_pv_ok_condition_for_parallel_); + } + // special for pv ok condition switch + if (this->pv_ok_condition_for_parallel_switch_) { + this->pv_ok_condition_for_parallel_switch_->publish_state(value_pv_ok_condition_for_parallel_ == 1); + } + if (this->pv_power_balance_) { + this->pv_power_balance_->publish_state(value_pv_power_balance_ == 1); + } + // special for power balance switch + if (this->pv_power_balance_switch_) { + this->pv_power_balance_switch_->publish_state(value_pv_power_balance_ == 1); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIGS: + if (this->grid_voltage_) { + this->grid_voltage_->publish_state(value_grid_voltage_); + } + if (this->grid_frequency_) { + this->grid_frequency_->publish_state(value_grid_frequency_); + } + if (this->ac_output_voltage_) { + this->ac_output_voltage_->publish_state(value_ac_output_voltage_); + } + if (this->ac_output_frequency_) { + this->ac_output_frequency_->publish_state(value_ac_output_frequency_); + } + if (this->ac_output_apparent_power_) { + this->ac_output_apparent_power_->publish_state(value_ac_output_apparent_power_); + } + if (this->ac_output_active_power_) { + this->ac_output_active_power_->publish_state(value_ac_output_active_power_); + } + if (this->output_load_percent_) { + this->output_load_percent_->publish_state(value_output_load_percent_); + } + if (this->bus_voltage_) { + this->bus_voltage_->publish_state(value_bus_voltage_); + } + if (this->battery_voltage_) { + this->battery_voltage_->publish_state(value_battery_voltage_); + } + if (this->battery_charging_current_) { + this->battery_charging_current_->publish_state(value_battery_charging_current_); + } + if (this->battery_capacity_percent_) { + this->battery_capacity_percent_->publish_state(value_battery_capacity_percent_); + } + if (this->inverter_heat_sink_temperature_) { + this->inverter_heat_sink_temperature_->publish_state(value_inverter_heat_sink_temperature_); + } + if (this->pv_input_current_for_battery_) { + this->pv_input_current_for_battery_->publish_state(value_pv_input_current_for_battery_); + } + if (this->pv_input_voltage_) { + this->pv_input_voltage_->publish_state(value_pv_input_voltage_); + } + if (this->battery_voltage_scc_) { + this->battery_voltage_scc_->publish_state(value_battery_voltage_scc_); + } + if (this->battery_discharge_current_) { + this->battery_discharge_current_->publish_state(value_battery_discharge_current_); + } + if (this->add_sbu_priority_version_) { + this->add_sbu_priority_version_->publish_state(value_add_sbu_priority_version_); + } + if (this->configuration_status_) { + this->configuration_status_->publish_state(value_configuration_status_); + } + if (this->scc_firmware_version_) { + this->scc_firmware_version_->publish_state(value_scc_firmware_version_); + } + if (this->load_status_) { + this->load_status_->publish_state(value_load_status_); + } + if (this->battery_voltage_to_steady_while_charging_) { + this->battery_voltage_to_steady_while_charging_->publish_state( + value_battery_voltage_to_steady_while_charging_); + } + if (this->charging_status_) { + this->charging_status_->publish_state(value_charging_status_); + } + if (this->scc_charging_status_) { + this->scc_charging_status_->publish_state(value_scc_charging_status_); + } + if (this->ac_charging_status_) { + this->ac_charging_status_->publish_state(value_ac_charging_status_); + } + if (this->battery_voltage_offset_for_fans_on_) { + this->battery_voltage_offset_for_fans_on_->publish_state(value_battery_voltage_offset_for_fans_on_ / 10.0f); + } //.1 scale + if (this->eeprom_version_) { + this->eeprom_version_->publish_state(value_eeprom_version_); + } + if (this->pv_charging_power_) { + this->pv_charging_power_->publish_state(value_pv_charging_power_); + } + if (this->charging_to_floating_mode_) { + this->charging_to_floating_mode_->publish_state(value_charging_to_floating_mode_); + } + if (this->switch_on_) { + this->switch_on_->publish_state(value_switch_on_); + } + if (this->dustproof_installed_) { + this->dustproof_installed_->publish_state(value_dustproof_installed_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QMOD: + if (this->device_mode_) { + mode = value_device_mode_; + this->device_mode_->publish_state(mode); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QFLAG: + if (this->silence_buzzer_open_buzzer_) { + this->silence_buzzer_open_buzzer_->publish_state(value_silence_buzzer_open_buzzer_); + } + if (this->overload_bypass_function_) { + this->overload_bypass_function_->publish_state(value_overload_bypass_function_); + } + if (this->lcd_escape_to_default_) { + this->lcd_escape_to_default_->publish_state(value_lcd_escape_to_default_); + } + if (this->overload_restart_function_) { + this->overload_restart_function_->publish_state(value_overload_restart_function_); + } + if (this->over_temperature_restart_function_) { + this->over_temperature_restart_function_->publish_state(value_over_temperature_restart_function_); + } + if (this->backlight_on_) { + this->backlight_on_->publish_state(value_backlight_on_); + } + if (this->alarm_on_when_primary_source_interrupt_) { + this->alarm_on_when_primary_source_interrupt_->publish_state(value_alarm_on_when_primary_source_interrupt_); + } + if (this->fault_code_record_) { + this->fault_code_record_->publish_state(value_fault_code_record_); + } + if (this->power_saving_) { + this->power_saving_->publish_state(value_power_saving_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIWS: + if (this->warnings_present_) { + this->warnings_present_->publish_state(value_warnings_present_); + } + if (this->faults_present_) { + this->faults_present_->publish_state(value_faults_present_); + } + if (this->warning_power_loss_) { + this->warning_power_loss_->publish_state(value_warning_power_loss_); + } + if (this->fault_inverter_fault_) { + this->fault_inverter_fault_->publish_state(value_fault_inverter_fault_); + } + if (this->fault_bus_over_) { + this->fault_bus_over_->publish_state(value_fault_bus_over_); + } + if (this->fault_bus_under_) { + this->fault_bus_under_->publish_state(value_fault_bus_under_); + } + if (this->fault_bus_soft_fail_) { + this->fault_bus_soft_fail_->publish_state(value_fault_bus_soft_fail_); + } + if (this->warning_line_fail_) { + this->warning_line_fail_->publish_state(value_warning_line_fail_); + } + if (this->fault_opvshort_) { + this->fault_opvshort_->publish_state(value_fault_opvshort_); + } + if (this->fault_inverter_voltage_too_low_) { + this->fault_inverter_voltage_too_low_->publish_state(value_fault_inverter_voltage_too_low_); + } + if (this->fault_inverter_voltage_too_high_) { + this->fault_inverter_voltage_too_high_->publish_state(value_fault_inverter_voltage_too_high_); + } + if (this->warning_over_temperature_) { + this->warning_over_temperature_->publish_state(value_warning_over_temperature_); + } + if (this->warning_fan_lock_) { + this->warning_fan_lock_->publish_state(value_warning_fan_lock_); + } + if (this->warning_battery_voltage_high_) { + this->warning_battery_voltage_high_->publish_state(value_warning_battery_voltage_high_); + } + if (this->warning_battery_low_alarm_) { + this->warning_battery_low_alarm_->publish_state(value_warning_battery_low_alarm_); + } + if (this->warning_battery_under_shutdown_) { + this->warning_battery_under_shutdown_->publish_state(value_warning_battery_under_shutdown_); + } + if (this->warning_battery_derating_) { + this->warning_battery_derating_->publish_state(value_warning_battery_derating_); + } + if (this->warning_over_load_) { + this->warning_over_load_->publish_state(value_warning_over_load_); + } + if (this->warning_eeprom_failed_) { + this->warning_eeprom_failed_->publish_state(value_warning_eeprom_failed_); + } + if (this->fault_inverter_over_current_) { + this->fault_inverter_over_current_->publish_state(value_fault_inverter_over_current_); + } + if (this->fault_inverter_soft_failed_) { + this->fault_inverter_soft_failed_->publish_state(value_fault_inverter_soft_failed_); + } + if (this->fault_self_test_failed_) { + this->fault_self_test_failed_->publish_state(value_fault_self_test_failed_); + } + if (this->fault_op_dc_voltage_over_) { + this->fault_op_dc_voltage_over_->publish_state(value_fault_op_dc_voltage_over_); + } + if (this->fault_battery_open_) { + this->fault_battery_open_->publish_state(value_fault_battery_open_); + } + if (this->fault_current_sensor_failed_) { + this->fault_current_sensor_failed_->publish_state(value_fault_current_sensor_failed_); + } + if (this->fault_battery_short_) { + this->fault_battery_short_->publish_state(value_fault_battery_short_); + } + if (this->warning_power_limit_) { + this->warning_power_limit_->publish_state(value_warning_power_limit_); + } + if (this->warning_pv_voltage_high_) { + this->warning_pv_voltage_high_->publish_state(value_warning_pv_voltage_high_); + } + if (this->fault_mppt_overload_) { + this->fault_mppt_overload_->publish_state(value_fault_mppt_overload_); + } + if (this->warning_mppt_overload_) { + this->warning_mppt_overload_->publish_state(value_warning_mppt_overload_); + } + if (this->warning_battery_too_low_to_charge_) { + this->warning_battery_too_low_to_charge_->publish_state(value_warning_battery_too_low_to_charge_); + } + if (this->fault_dc_dc_over_current_) { + this->fault_dc_dc_over_current_->publish_state(value_fault_dc_dc_over_current_); + } + if (this->fault_code_) { + this->fault_code_->publish_state(value_fault_code_); + } + if (this->warnung_low_pv_energy_) { + this->warnung_low_pv_energy_->publish_state(value_warnung_low_pv_energy_); + } + if (this->warning_high_ac_input_during_bus_soft_start_) { + this->warning_high_ac_input_during_bus_soft_start_->publish_state( + value_warning_high_ac_input_during_bus_soft_start_); + } + if (this->warning_battery_equalization_) { + this->warning_battery_equalization_->publish_state(value_warning_battery_equalization_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QT: + this->state_ = STATE_IDLE; + break; + case POLLING_QMN: + this->state_ = STATE_IDLE; + break; + } + } + + if (this->state_ == STATE_POLL_CHECKED) { + bool enabled = true; + std::string fc; + char tmp[PIPSOLAR_READ_BUFFER_LENGTH]; + sprintf(tmp, "%s", this->read_buffer_); + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + ESP_LOGD(TAG, "Decode QPIRI"); + sscanf(tmp, "(%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d %d %d %d %d %f %d %d", // NOLINT + &value_grid_rating_voltage_, &value_grid_rating_current_, &value_ac_output_rating_voltage_, // NOLINT + &value_ac_output_rating_frequency_, &value_ac_output_rating_current_, // NOLINT + &value_ac_output_rating_apparent_power_, &value_ac_output_rating_active_power_, // NOLINT + &value_battery_rating_voltage_, &value_battery_recharge_voltage_, // NOLINT + &value_battery_under_voltage_, &value_battery_bulk_voltage_, &value_battery_float_voltage_, // NOLINT + &value_battery_type_, &value_current_max_ac_charging_current_, // NOLINT + &value_current_max_charging_current_, &value_input_voltage_range_, // NOLINT + &value_output_source_priority_, &value_charger_source_priority_, &value_parallel_max_num_, // NOLINT + &value_machine_type_, &value_topology_, &value_output_mode_, // NOLINT + &value_battery_redischarge_voltage_, &value_pv_ok_condition_for_parallel_, // NOLINT + &value_pv_power_balance_); // NOLINT + if (this->last_qpiri_) { + this->last_qpiri_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIGS: + ESP_LOGD(TAG, "Decode QPIGS"); + sscanf( // NOLINT + tmp, // NOLINT + "(%f %f %f %f %d %d %d %d %f %d %d %d %d %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT + &value_grid_voltage_, &value_grid_frequency_, &value_ac_output_voltage_, // NOLINT + &value_ac_output_frequency_, // NOLINT + &value_ac_output_apparent_power_, &value_ac_output_active_power_, &value_output_load_percent_, // NOLINT + &value_bus_voltage_, &value_battery_voltage_, &value_battery_charging_current_, // NOLINT + &value_battery_capacity_percent_, &value_inverter_heat_sink_temperature_, // NOLINT + &value_pv_input_current_for_battery_, &value_pv_input_voltage_, &value_battery_voltage_scc_, // NOLINT + &value_battery_discharge_current_, &value_add_sbu_priority_version_, // NOLINT + &value_configuration_status_, &value_scc_firmware_version_, &value_load_status_, // NOLINT + &value_battery_voltage_to_steady_while_charging_, &value_charging_status_, // NOLINT + &value_scc_charging_status_, &value_ac_charging_status_, // NOLINT + &value_battery_voltage_offset_for_fans_on_, &value_eeprom_version_, &value_pv_charging_power_, // NOLINT + &value_charging_to_floating_mode_, &value_switch_on_, // NOLINT + &value_dustproof_installed_); // NOLINT + if (this->last_qpigs_) { + this->last_qpigs_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMOD: + ESP_LOGD(TAG, "Decode QMOD"); + this->value_device_mode_ = char(this->read_buffer_[1]); + if (this->last_qmod_) { + this->last_qmod_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QFLAG: + ESP_LOGD(TAG, "Decode QFLAG"); + // result like:"(EbkuvxzDajy" + // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value + for (int i = 1; i < strlen(tmp); i++) { + switch (tmp[i]) { + case 'E': + enabled = true; + break; + case 'D': + enabled = false; + break; + case 'a': + this->value_silence_buzzer_open_buzzer_ = enabled; + break; + case 'b': + this->value_overload_bypass_function_ = enabled; + break; + case 'k': + this->value_lcd_escape_to_default_ = enabled; + break; + case 'u': + this->value_overload_restart_function_ = enabled; + break; + case 'v': + this->value_over_temperature_restart_function_ = enabled; + break; + case 'x': + this->value_backlight_on_ = enabled; + break; + case 'y': + this->value_alarm_on_when_primary_source_interrupt_ = enabled; + break; + case 'z': + this->value_fault_code_record_ = enabled; + break; + case 'j': + this->value_power_saving_ = enabled; + break; + } + } + if (this->last_qflag_) { + this->last_qflag_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIWS: + ESP_LOGD(TAG, "Decode QPIWS"); + // '(00000000000000000000000000000000' + // iterate over all available flag (as not all models have all flags, but at least in the same order) + this->value_warnings_present_ = false; + this->value_faults_present_ = true; + + for (int i = 1; i < strlen(tmp); i++) { + enabled = tmp[i] == '1'; + switch (i) { + case 1: + this->value_warning_power_loss_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 2: + this->value_fault_inverter_fault_ = enabled; + this->value_faults_present_ += enabled; + break; + case 3: + this->value_fault_bus_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 4: + this->value_fault_bus_under_ = enabled; + this->value_faults_present_ += enabled; + break; + case 5: + this->value_fault_bus_soft_fail_ = enabled; + this->value_faults_present_ += enabled; + break; + case 6: + this->value_warning_line_fail_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 7: + this->value_fault_opvshort_ = enabled; + this->value_faults_present_ += enabled; + break; + case 8: + this->value_fault_inverter_voltage_too_low_ = enabled; + this->value_faults_present_ += enabled; + break; + case 9: + this->value_fault_inverter_voltage_too_high_ = enabled; + this->value_faults_present_ += enabled; + break; + case 10: + this->value_warning_over_temperature_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 11: + this->value_warning_fan_lock_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 12: + this->value_warning_battery_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 13: + this->value_warning_battery_low_alarm_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 15: + this->value_warning_battery_under_shutdown_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 16: + this->value_warning_battery_derating_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 17: + this->value_warning_over_load_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 18: + this->value_warning_eeprom_failed_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 19: + this->value_fault_inverter_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 20: + this->value_fault_inverter_soft_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 21: + this->value_fault_self_test_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 22: + this->value_fault_op_dc_voltage_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 23: + this->value_fault_battery_open_ = enabled; + this->value_faults_present_ += enabled; + break; + case 24: + this->value_fault_current_sensor_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 25: + this->value_fault_battery_short_ = enabled; + this->value_faults_present_ += enabled; + break; + case 26: + this->value_warning_power_limit_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 27: + this->value_warning_pv_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 28: + this->value_fault_mppt_overload_ = enabled; + this->value_faults_present_ += enabled; + break; + case 29: + this->value_warning_mppt_overload_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 30: + this->value_warning_battery_too_low_to_charge_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 31: + this->value_fault_dc_dc_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 32: + fc = tmp[i]; + fc += tmp[i + 1]; + this->value_fault_code_ = strtol(fc.c_str(), nullptr, 10); + break; + case 34: + this->value_warnung_low_pv_energy_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 35: + this->value_warning_high_ac_input_during_bus_soft_start_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 36: + this->value_warning_battery_equalization_ = enabled; + this->value_warnings_present_ += enabled; + break; + } + } + if (this->last_qpiws_) { + this->last_qpiws_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QT: + ESP_LOGD(TAG, "Decode QT"); + if (this->last_qt_) { + this->last_qt_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMN: + ESP_LOGD(TAG, "Decode QMN"); + if (this->last_qmn_) { + this->last_qmn_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + default: + this->state_ = STATE_IDLE; + break; + } + return; + } + + if (this->state_ == STATE_POLL_COMPLETE) { + if (this->check_incoming_crc_()) { + if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && + this->read_buffer_[3] == 'K') { + this->state_ = STATE_IDLE; + return; + } + // crc ok + this->state_ = STATE_POLL_CHECKED; + return; + } else { + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == PIPSOLAR_READ_BUFFER_LENGTH) { + this->read_pos_ = 0; + this->empty_uart_buffer_(); + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + + // end of answer + if (byte == 0x0D) { + this->read_buffer_[this->read_pos_] = 0; + this->empty_uart_buffer_(); + if (this->state_ == STATE_POLL) { + this->state_ = STATE_POLL_COMPLETE; + } + if (this->state_ == STATE_COMMAND) { + this->state_ = STATE_COMMAND_COMPLETE; + } + } + } // available + } + if (this->state_ == STATE_COMMAND) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + this->command_start_millis_ = millis(); + ESP_LOGD(TAG, "timeout command from queue: %s", command); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + return; + } else { + } + } + if (this->state_ == STATE_POLL) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + ESP_LOGD(TAG, "timeout command to poll: %s", this->used_polling_commands_[this->last_polling_command_].command); + this->state_ = STATE_IDLE; + } else { + } + } +} + +uint8_t Pipsolar::check_incoming_length_(uint8_t length) { + if (this->read_pos_ - 3 == length) { + return 1; + } + return 0; +} + +uint8_t Pipsolar::check_incoming_crc_() { + uint16_t crc16; + crc16 = calc_crc_(read_buffer_, read_pos_ - 3); + ESP_LOGD(TAG, "checking crc on incoming message"); + if (((uint8_t)((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && + ((uint8_t)((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { + ESP_LOGD(TAG, "CRC OK"); + read_buffer_[read_pos_ - 1] = 0; + read_buffer_[read_pos_ - 2] = 0; + read_buffer_[read_pos_ - 3] = 0; + return 1; + } + ESP_LOGD(TAG, "CRC NOK expected: %X %X but got: %X %X", ((uint8_t)((crc16) >> 8)), ((uint8_t)((crc16) &0xff)), + read_buffer_[read_pos_ - 3], read_buffer_[read_pos_ - 2]); + return 0; +} + +// send next command used +uint8_t Pipsolar::send_next_command_() { + uint16_t crc16; + if (this->command_queue_[this->command_queue_position_].length() != 0) { + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + uint8_t byte_command[16]; + uint8_t length = this->command_queue_[this->command_queue_position_].length(); + for (uint8_t i = 0; i < length; i++) { + byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); + } + this->state_ = STATE_COMMAND; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_crc_(byte_command, length); + this->write_str(command); + // checksum + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length); + return 1; + } + return 0; +} + +void Pipsolar::send_next_poll_() { + uint16_t crc16; + this->last_polling_command_ = (this->last_polling_command_ + 1) % 15; + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + this->last_polling_command_ = 0; + } + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + // no command specified + return; + } + this->state_ = STATE_POLL; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_crc_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + this->write_array(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + // checksum + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending polling command : %s with length %d", + this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); +} + +void Pipsolar::queue_command_(const char *command, byte length) { + uint8_t next_position = command_queue_position_; + for (uint8_t i = 0; i < COMMAND_QUEUE_LENGTH; i++) { + uint8_t testposition = (next_position + i) % COMMAND_QUEUE_LENGTH; + if (command_queue_[testposition].length() == 0) { + command_queue_[testposition] = command; + ESP_LOGD(TAG, "Command queued successfully: %s with length %u at position %d", command, + command_queue_[testposition].length(), testposition); + return; + } + } + ESP_LOGD(TAG, "Command queue full dropping command: %s", command); +} + +void Pipsolar::switch_command(const std::string &command) { + ESP_LOGD(TAG, "got command: %s", command.c_str()); + queue_command_(command.c_str(), command.length()); +} +void Pipsolar::dump_config() { + ESP_LOGCONFIG(TAG, "Pipsolar:"); + ESP_LOGCONFIG(TAG, "used commands:"); + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length != 0) { + ESP_LOGCONFIG(TAG, "%s", used_polling_command.command); + } + } +} +void Pipsolar::update() {} + +void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) { + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length == strlen(command)) { + uint8_t len = strlen(command); + if (memcmp(used_polling_command.command, command, len) == 0) { + return; + } + } + if (used_polling_command.length == 0) { + size_t length = strlen(command) + 1; + const char *beg = command; + const char *end = command + length; + used_polling_command.command = new uint8_t[length]; + size_t i = 0; + for (; beg != end; ++beg, ++i) { + used_polling_command.command[i] = (uint8_t)(*beg); + } + used_polling_command.errors = 0; + used_polling_command.identifier = polling_command; + used_polling_command.length = length - 1; + return; + } + } +} + +uint16_t Pipsolar::calc_crc_(uint8_t *msg, int n) { + // Initial value. xmodem uses 0xFFFF but this example + // requires an initial value of zero. + uint16_t x = 0; + while (n--) { + x = crc_xmodem_update_(x, (uint16_t) *msg++); + } + return (x); +} + +// See bottom of this page: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html +// Polynomial: x^16 + x^12 + x^5 + 1 (0x1021) +uint16_t Pipsolar::crc_xmodem_update_(uint16_t crc, uint8_t data) { + int i; + crc = crc ^ ((uint16_t) data << 8); + for (i = 0; i < 8; i++) { + if (crc & 0x8000) + crc = (crc << 1) ^ 0x1021; //(polynomial = 0x1021) + else + crc <<= 1; + } + return crc; +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h new file mode 100644 index 0000000000..508036c7de --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.h @@ -0,0 +1,223 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +enum ENUMPollingCommand { + POLLING_QPIRI = 0, + POLLING_QPIGS = 1, + POLLING_QMOD = 2, + POLLING_QFLAG = 3, + POLLING_QPIWS = 4, + POLLING_QT = 5, + POLLING_QMN = 6, +}; +struct PollingCommand { + uint8_t *command; + uint8_t length = 0; + uint8_t errors; + ENUMPollingCommand identifier; +}; + +#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ + protected: \ + value_type value_##name##_; \ + PIPSOLAR_ENTITY_(type, name, polling_command) + +#define PIPSOLAR_ENTITY_(type, name, polling_command) \ + protected: \ + type *name##_{}; /* NOLINT */ \ +\ + public: \ + void set_##name(type *name) { /* NOLINT */ \ + this->name##_ = name; \ + this->add_polling_command_(#polling_command, POLLING_##polling_command); \ + } + +#define PIPSOLAR_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type) +#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command) +#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type) +#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type) +#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command) + +class Pipsolar : public uart::UARTDevice, public PollingComponent { + // QPIGS values + PIPSOLAR_SENSOR(grid_voltage, QPIGS, float) + PIPSOLAR_SENSOR(grid_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int) + PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int) + PIPSOLAR_SENSOR(output_load_percent, QPIGS, int) + PIPSOLAR_SENSOR(bus_voltage, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) + PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) + PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) + PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale + PIPSOLAR_SENSOR(eeprom_version, QPIGS, int) + PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int) + + // QPIRI values + PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int) + PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int) + PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_type, QPIRI, int) + PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int) + PIPSOLAR_SENSOR(output_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int) + PIPSOLAR_SENSOR(machine_type, QPIRI, int) + PIPSOLAR_SENSOR(topology, QPIRI, int) + PIPSOLAR_SENSOR(output_mode, QPIRI, int) + PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int) + PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int) + + // QMOD values + PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char) + + // QFLAG values + PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int) + + // QPIWS values + PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int) + PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool) + + PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) + PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI) + PIPSOLAR_TEXT_SENSOR(last_qmod, QMOD) + PIPSOLAR_TEXT_SENSOR(last_qflag, QFLAG) + PIPSOLAR_TEXT_SENSOR(last_qpiws, QPIWS) + PIPSOLAR_TEXT_SENSOR(last_qt, QT) + PIPSOLAR_TEXT_SENSOR(last_qmn, QMN) + + PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) + PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) + PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) + PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) + + void switch_command(const std::string &command); + void setup() override; + void loop() override; + void dump_config() override; + void update() override; + + protected: + static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length + static const size_t COMMAND_QUEUE_LENGTH = 10; + static const size_t COMMAND_TIMEOUT = 5000; + uint32_t last_poll_ = 0; + void add_polling_command_(const char *command, ENUMPollingCommand polling_command); + void empty_uart_buffer_(); + uint8_t check_incoming_crc_(); + uint8_t check_incoming_length_(uint8_t length); + uint16_t calc_crc_(uint8_t *msg, int n); + uint16_t crc_xmodem_update_(uint16_t crc, uint8_t data); + uint8_t send_next_command_(); + void send_next_poll_(); + void queue_command_(const char *command, byte length); + std::string command_queue_[COMMAND_QUEUE_LENGTH]; + uint8_t command_queue_position_ = 0; + uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + uint32_t command_start_millis_ = 0; + uint8_t state_; + enum State { + STATE_IDLE = 0, + STATE_POLL = 1, + STATE_COMMAND = 2, + STATE_POLL_COMPLETE = 3, + STATE_COMMAND_COMPLETE = 4, + STATE_POLL_CHECKED = 5, + STATE_POLL_DECODED = 6, + }; + + uint8_t last_polling_command_ = 0; + PollingCommand used_polling_commands_[15]; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py new file mode 100644 index 0000000000..5e4dd6c40c --- /dev/null +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -0,0 +1,220 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_EMPTY, + UNIT_VOLT_AMPS, + UNIT_WATT, + CONF_BUS_VOLTAGE, + CONF_BATTERY_VOLTAGE, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +# QPIRI sensors +CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" +CONF_GRID_RATING_CURRENT = "grid_rating_current" +CONF_AC_OUTPUT_RATING_VOLTAGE = "ac_output_rating_voltage" +CONF_AC_OUTPUT_RATING_FREQUENCY = "ac_output_rating_frequency" +CONF_AC_OUTPUT_RATING_CURRENT = "ac_output_rating_current" +CONF_AC_OUTPUT_RATING_APPARENT_POWER = "ac_output_rating_apparent_power" +CONF_AC_OUTPUT_RATING_ACTIVE_POWER = "ac_output_rating_active_power" +CONF_BATTERY_RATING_VOLTAGE = "battery_rating_voltage" +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_BULK_VOLTAGE = "battery_bulk_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_PARALLEL_MAX_NUM = "parallel_max_num" +CONF_MACHINE_TYPE = "machine_type" +CONF_TOPOLOGY = "topology" +CONF_OUTPUT_MODE = "output_mode" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +CONF_GRID_VOLTAGE = "grid_voltage" +CONF_GRID_FREQUENCY = "grid_frequency" +CONF_AC_OUTPUT_VOLTAGE = "ac_output_voltage" +CONF_AC_OUTPUT_FREQUENCY = "ac_output_frequency" +CONF_AC_OUTPUT_APPARENT_POWER = "ac_output_apparent_power" +CONF_AC_OUTPUT_ACTIVE_POWER = "ac_output_active_power" +CONF_OUTPUT_LOAD_PERCENT = "output_load_percent" +CONF_BATTERY_CHARGING_CURRENT = "battery_charging_current" +CONF_BATTERY_CAPACITY_PERCENT = "battery_capacity_percent" +CONF_INVERTER_HEAT_SINK_TEMPERATURE = "inverter_heat_sink_temperature" +CONF_PV_INPUT_CURRENT_FOR_BATTERY = "pv_input_current_for_battery" +CONF_PV_INPUT_VOLTAGE = "pv_input_voltage" +CONF_BATTERY_VOLTAGE_SCC = "battery_voltage_scc" +CONF_BATTERY_DISCHARGE_CURRENT = "battery_discharge_current" +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON = "battery_voltage_offset_for_fans_on" +CONF_EEPROM_VERSION = "eeprom_version" +CONF_PV_CHARGING_POWER = "pv_charging_power" + +TYPES = { + CONF_GRID_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_MACHINE_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_TOPOLOGY: sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY), + CONF_OUTPUT_MODE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_POWER_BALANCE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_GRID_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BUS_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + ), + CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_EEPROM_VERSION: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_CHARGING_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): schema for type, schema in TYPES.items()} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, _ in TYPES.items(): + if type in config: + conf = config[type] + sens = await sensor.new_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py new file mode 100644 index 0000000000..5ff33b10ff --- /dev/null +++ b/esphome/components/pipsolar/switch/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ICON_POWER, +) +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" +CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" +CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" +CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +TYPES = { + CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), + CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), + CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), + CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), + CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), +} + +PipsolarSwitch = pipsolar_ns.class_("PipsolarSwitch", switch.Switch, cg.Component) + +PIPSWITCH_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PipsolarSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Pipsolar switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon, + } +).extend(cv.COMPONENT_SCHEMA) + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): PIPSWITCH_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (on, off) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + cg.add(getattr(paren, f"set_{type}_switch")(var)) + cg.add(var.set_parent(paren)) + cg.add(var.set_on_command(on)) + if off is not None: + cg.add(var.set_off_command(off)) diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp new file mode 100644 index 0000000000..7eaeac1c2d --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp @@ -0,0 +1,24 @@ +#include "pipsolar_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.switch"; + +void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); } +void PipsolarSwitch::write_state(bool state) { + if (state) { + if (this->on_command_.length() > 0) { + this->parent_->switch_command(this->on_command_); + } + } else { + if (this->off_command_.length() > 0) { + this->parent_->switch_command(this->off_command_); + } + } +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.h b/esphome/components/pipsolar/switch/pipsolar_switch.h new file mode 100644 index 0000000000..3fe4c7dfa1 --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarSwitch : public switch_::Switch, public Component { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void set_on_command(std::string command) { this->on_command_ = std::move(command); }; + void set_off_command(std::string command) { this->off_command_ = std::move(command); }; + void dump_config() override; + + protected: + void write_state(bool state) override; + std::string on_command_; + std::string off_command_; + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/__init__.py b/esphome/components/pipsolar/text_sensor/__init__.py new file mode 100644 index 0000000000..fe6c4979f3 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CONF_DEVICE_MODE = "device_mode" +CONF_LAST_QPIGS = "last_qpigs" +CONF_LAST_QPIRI = "last_qpiri" +CONF_LAST_QMOD = "last_qmod" +CONF_LAST_QFLAG = "last_qflag" +CONF_LAST_QPIWS = "last_qpiws" +CONF_LAST_QT = "last_qt" +CONF_LAST_QMN = "last_qmn" + +PipsolarTextSensor = pipsolar_ns.class_( + "PipsolarTextSensor", text_sensor.TextSensor, cg.Component +) + +TYPES = [ + CONF_DEVICE_MODE, + CONF_LAST_QPIGS, + CONF_LAST_QPIRI, + CONF_LAST_QMOD, + CONF_LAST_QFLAG, + CONF_LAST_QPIWS, + CONF_LAST_QT, + CONF_LAST_QMN, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): text_sensor.TEXT_SENSOR_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PipsolarTextSensor)} + ) + for type in TYPES + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type in TYPES: + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + await cg.register_component(var, conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp new file mode 100644 index 0000000000..ee1fe2d1d8 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp @@ -0,0 +1,13 @@ +#include "pipsolar_textsensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.text_sensor"; + +void PipsolarTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pipsolar TextSensor", this); } + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h new file mode 100644 index 0000000000..871f6d8dee --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarTextSensor : public Component, public text_sensor::TextSensor { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void dump_config() override; + + protected: + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 2857e8ca5f..e52e8cc33c 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -56,6 +56,9 @@ time: tuya: time_id: sntp_time +pipsolar: + id: inverter0 + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -63,6 +66,140 @@ sensor: - platform: tuya id: tuya_sensor sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power - platform: "hrxl_maxsonar_wr" name: "Rainwater Tank Level" filters: @@ -95,6 +232,59 @@ binary_sensor: - platform: tuya id: tuya_binary_sensor sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on - platform: template id: ar1 lambda: 'return {};' @@ -131,6 +321,20 @@ switch: - platform: tuya id: tuya_switch switch_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance light: - platform: fastled_clockless @@ -204,6 +408,30 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out esp32_camera: name: ESP-32 Camera data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19]