diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d5c7f799..8443d814 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,6 +4,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + - pip8048 pull_request: schedule: - cron: 0 12 * * * @@ -23,10 +24,10 @@ jobs: steps: - name: ⤵️ Check out configuration from GitHub uses: actions/checkout@v2 - - name: Setup Python 3.8 + - name: Setup Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel @@ -36,10 +37,16 @@ jobs: - name: Write secrets.yaml shell: bash run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > secrets.yaml' + - name: Write tests/secrets.yaml + shell: bash + run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > tests/secrets.yaml' - run: | esphome config esp32-example.yaml - run: | esphome config esp8266-example.yaml + esphome config tests/esp8266-test-ping.yaml + esphome config tests/esp8266-test-pong.yaml + esphome config tests/esp8266-test-protocols.yaml esphome-compile: runs-on: ubuntu-latest @@ -59,10 +66,10 @@ jobs: path: .pioenvs key: esphome-compile-pioenvs-${{ hashFiles('*.yaml') }} restore-keys: esphome-compile-pioenvs- - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel diff --git a/.gitignore b/.gitignore index dd71e158..862848c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea/ -secret.yaml +secrets.yaml .esphome/ **/.pioenvs/ **/.piolibdeps/ diff --git a/components/pipsolar/__init__.py b/components/pipsolar/__init__.py new file mode 100644 index 00000000..9da6af19 --- /dev/null +++ b/components/pipsolar/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@andreashergert1984"] +AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "output", "select"] +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.Schema( + { + 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/components/pipsolar/binary_sensor/__init__.py b/components/pipsolar/binary_sensor/__init__.py new file mode 100644 index 00000000..0cd98a0e --- /dev/null +++ b/components/pipsolar/binary_sensor/__init__.py @@ -0,0 +1,150 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +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_WARNING_LOW_PV_ENERGY = "warning_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" + +# QBATCD binary sensors + +CONF_DISCHARGE_ONOFF = "discharge_onoff" +CONF_DISCHARGE_WITH_STANDBY_ONOFF = "discharge_with_standby_onoff" +CONF_CHARGE_ONOFF = "charge_onoff" + +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_WARNING_LOW_PV_ENERGY, + CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, + CONF_WARNING_BATTERY_EQUALIZATION, + CONF_DISCHARGE_ONOFF, + CONF_DISCHARGE_WITH_STANDBY_ONOFF, + CONF_CHARGE_ONOFF, +] + +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] + var = await binary_sensor.new_binary_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/components/pipsolar/output/__init__.py b/components/pipsolar/output/__init__.py new file mode 100644 index 00000000..161a7f09 --- /dev/null +++ b/components/pipsolar/output/__init__.py @@ -0,0 +1,111 @@ +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 CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, 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_BULK_VOLTAGE = "battery_bulk_voltage" +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_BULK_VOLTAGE: ( + [44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0], + "PCVV%02.1f", + ), + 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, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120], "MUCHGC%04.0f"), + CONF_CURRENT_MAX_CHARGING_CURRENT: ([ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 ], "MCHGC%03.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/components/pipsolar/output/pipsolar_output.cpp b/components/pipsolar/output/pipsolar_output.cpp new file mode 100644 index 00000000..adec2096 --- /dev/null +++ b/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[12]; + 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 / %03.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/components/pipsolar/output/pipsolar_output.h b/components/pipsolar/output/pipsolar_output.h new file mode 100644 index 00000000..fe783cf0 --- /dev/null +++ b/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(const std::string &command) { this->set_command_ = 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/components/pipsolar/pipsolar.cpp b/components/pipsolar/pipsolar.cpp new file mode 100644 index 00000000..7d6ed639 --- /dev/null +++ b/components/pipsolar/pipsolar.cpp @@ -0,0 +1,1021 @@ +#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_); + } + // select for current_max_ac_charging_current + if (this->current_max_ac_charging_current_select_) { + std::string value = esphome::to_string(value_current_max_ac_charging_current_); + this->current_max_ac_charging_current_select_->map_and_publish(value); + } + + if (this->current_max_charging_current_) { + this->current_max_charging_current_->publish_state(value_current_max_charging_current_); + } + //select for current_max_charging_current + if (this->current_max_charging_current_select_) { + std::string value = esphome::to_string(value_current_max_charging_current_); + this->current_max_charging_current_select_->map_and_publish(value); + } + + 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 select + if (this->output_source_priority_select_) { + std::string value = esphome::to_string(value_output_source_priority_); + this->output_source_priority_select_->map_and_publish(value); + } + // 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_); + } + // special for charger source priority select + if (this->charger_source_priority_select_) { + std::string value = esphome::to_string(value_charger_source_priority_); + this->charger_source_priority_select_->map_and_publish(value); + } + + 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->pv1_input_current_) { + this->pv1_input_current_->publish_state(value_pv1_input_current_); + } + if (this->pv1_input_voltage_) { + this->pv1_input_voltage_->publish_state(value_pv1_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->pv1_charging_power_) { + this->pv1_charging_power_->publish_state(value_pv1_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_QPIGS2: + if (this->pv2_input_current_) { + this->pv2_input_current_->publish_state(value_pv2_input_current_); + } + if (this->pv2_input_voltage_) { + this->pv2_input_voltage_->publish_state(value_pv2_input_voltage_); + } + if (this->pv2_charging_power_) { + this->pv2_charging_power_->publish_state(value_pv2_charging_power_); + } + 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_QBATCD: + if (this->discharge_onoff_) { + this->discharge_onoff_->publish_state(value_discharge_onoff_); + } + if (this->discharge_with_standby_onoff_) { + this->discharge_with_standby_onoff_->publish_state(value_discharge_with_standby_onoff_); + } + if (this->charge_onoff_) { + this->charge_onoff_->publish_state(value_charge_onoff_); + } + if (this->charging_discharging_control_select_) { + this->charging_discharging_control_select_->map_and_publish(value_charging_discharging_control_select_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QT: + 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->current_max_ac_charging_current_select_ = value_current_max_ac_charging_current_; + this->current_max_charging_current_select_ = value_current_max_charging_current_; + */ + 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 %f %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_pv1_input_current_, &value_pv1_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_pv1_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_QPIGS2: + ESP_LOGD(TAG, "Decode QPIGS2"); + sscanf( // NOLINT + tmp, // NOLINT + "(%f %f %d", // NOLINT + &value_pv2_input_current_, &value_pv2_input_voltage_, &value_pv2_charging_power_); // NOLINT + if (this->last_qpigs2_) { + this->last_qpigs2_->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 (size_t 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_ = false; + + for (size_t 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_ = parse_number(fc).value_or(0); + 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; + case POLLING_QBATCD: + ESP_LOGD(TAG, "Decode QBATCD"); + // '(000' + for (size_t i = 1; i < strlen(tmp); i++) { + enabled = tmp[i] == '1'; + switch (i) { + case 1: + this->value_discharge_onoff_ = enabled; + break; + case 2: + this->value_discharge_with_standby_onoff_ = enabled; + break; + case 3: + this->value_charge_onoff_ = enabled; + break; + } + } + this->value_charging_discharging_control_select_ = tmp; + if (this->last_qbatcd_) { + this->last_qbatcd_->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 = cal_crc_half_(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 = cal_crc_half_(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 = cal_crc_half_(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, uint8_t 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]; // NOLINT(cppcoreguidelines-owning-memory) + 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::cal_crc_half_(uint8_t *msg, uint8_t len) { + uint16_t crc; + + uint8_t da; + uint8_t *ptr; + uint8_t b_crc_hign; + uint8_t b_crc_low; + + uint16_t crc_ta[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef}; + + ptr = msg; + crc = 0; + + while (len-- != 0) { + da = ((uint8_t)(crc >> 8)) >> 4; + crc <<= 4; + crc ^= crc_ta[da ^ (*ptr >> 4)]; + da = ((uint8_t)(crc >> 8)) >> 4; + crc <<= 4; + crc ^= crc_ta[da ^ (*ptr & 0x0f)]; + ptr++; + } + + b_crc_low = crc; + b_crc_hign = (uint8_t)(crc >> 8); + + if (b_crc_low == 0x28 || b_crc_low == 0x0d || b_crc_low == 0x0a) + b_crc_low++; + if (b_crc_hign == 0x28 || b_crc_hign == 0x0d || b_crc_hign == 0x0a) + b_crc_hign++; + + crc = ((uint16_t) b_crc_hign) << 8; + crc += b_crc_low; + return (crc); +} + +} // namespace pipsolar +} // namespace esphome diff --git a/components/pipsolar/pipsolar.h b/components/pipsolar/pipsolar.h new file mode 100644 index 00000000..36e198a4 --- /dev/null +++ b/components/pipsolar/pipsolar.h @@ -0,0 +1,252 @@ +#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/select/select.h" +#include "esphome/components/pipsolar/select/pipsolar_select.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 { +class PipsolarSelect; + +enum ENUMPollingCommand { + POLLING_QPIRI = 0, + POLLING_QPIGS = 1, + POLLING_QPIGS2 = 2, + POLLING_QMOD = 3, + POLLING_QFLAG = 4, + POLLING_QPIWS = 5, + POLLING_QT = 6, + POLLING_QMN = 7, + POLLING_QBATCD = 8, +}; +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_SELECT(name, polling_command) PIPSOLAR_ENTITY_(pipsolar::PipsolarSelect, name, polling_command) +#define PIPSOLAR_VALUED_SELECT(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(pipsolar::PipsolarSelect, name, polling_command, value_type) +#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(pv1_input_current, QPIGS, float) + PIPSOLAR_SENSOR(pv1_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(pv1_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) + + // QPIGS2 values + + PIPSOLAR_SENSOR(pv2_input_current, QPIGS2, float) + PIPSOLAR_SENSOR(pv2_input_voltage, QPIGS2, float) + PIPSOLAR_SENSOR(pv2_charging_power, QPIGS2, 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) + + // QBATCD values + PIPSOLAR_BINARY_SENSOR(discharge_onoff, QBATCD, bool) + PIPSOLAR_BINARY_SENSOR(discharge_with_standby_onoff, QBATCD, bool) + PIPSOLAR_BINARY_SENSOR(charge_onoff, QBATCD, bool) + + PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) + PIPSOLAR_TEXT_SENSOR(last_qpigs2, QPIGS2) + 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_TEXT_SENSOR(last_qbatcd, QBATCD) + + 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) + + PIPSOLAR_SELECT(output_source_priority_select, QPIRI) + PIPSOLAR_SELECT(charger_source_priority_select, QPIRI) + + PIPSOLAR_SELECT(current_max_ac_charging_current_select, QPIRI) + PIPSOLAR_SELECT(current_max_charging_current_select, QPIRI) + + PIPSOLAR_VALUED_SELECT(charging_discharging_control_select, QBATCD, std::string) + + void switch_command(const std::string &command); + void setup() override; + void loop() override; + void dump_config() override; + void update() override; + + protected: + friend class PipsolarSelect; + static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 130; // 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 cal_crc_half_(uint8_t *msg, uint8_t len); + uint8_t send_next_command_(); + void send_next_poll_(); + void queue_command_(const char *command, uint8_t 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/components/pipsolar/select/__init__.py b/components/pipsolar/select/__init__.py new file mode 100644 index 00000000..763a1348 --- /dev/null +++ b/components/pipsolar/select/__init__.py @@ -0,0 +1,176 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import CONF_ID, CONF_OPTIMISTIC + +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CODEOWNERS = ["@andreashergert1984"] +CONF_OPTIONSMAP = "optionsmap" +CONF_STATUSMAP = "statusmap" + +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_CHARGING_DISCHARGING_CONTROL = "charging_discharging_control" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" + + +PipsolarSelect = pipsolar_ns.class_("PipsolarSelect", cg.Component, select.Select) + + +def ensure_option_map(): + def validator(value): + cv.check_not_templatable(value) + option = cv.All(cv.string_strict) + mapping = cv.All(cv.string_strict) + options_map_schema = cv.Schema({option: mapping}) + value = options_map_schema(value) + + all_values = list(value.values()) + unique_values = set(value.values()) + if len(all_values) != len(unique_values): + raise cv.Invalid("Mapping values must be unique.") + + return value + + return validator + + +# def register_count_value_type_min(value): +# reg_count = value.get(CONF_REGISTER_COUNT) +# if reg_count is not None: +# value_type = value[CONF_VALUE_TYPE] +# min_register_count = TYPE_REGISTER_MAP[value_type] +# if min_register_count > reg_count: +# raise cv.Invalid( +# f"Value type {value_type} needs at least {min_register_count} registers" +# ) +# return value + +# INTEGER_SENSOR_VALUE_TYPE = { +# key: value for key, value in SENSOR_VALUE_TYPE.items() if not key.startswith("FP") +# } + +# CONFIG_SCHEMA = cv.All( +# select.SELECT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( +# { +# cv.GenerateID(): cv.declare_id(PipSolarSelect), +# cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), +# cv.Required(CONF_ADDRESS): cv.positive_int, +# cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( +# INTEGER_SENSOR_VALUE_TYPE +# ), +# cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, +# cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, +# cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, +# cv.Required(CONF_OPTIONSMAP): ensure_option_map(), +# cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, +# cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, +# cv.Optional(CONF_LAMBDA): cv.returning_lambda, +# cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, +# }, +# ), +# register_count_value_type_min, +# ) + +TYPES = { + CONF_OUTPUT_SOURCE_PRIORITY: ("POP00", None), + CONF_CHARGER_SOURCE_PRIORITY: ("PCP03", None), + CONF_CHARGING_DISCHARGING_CONTROL: ("PBATCD111", None), + CONF_CURRENT_MAX_CHARGING_CURRENT: ("MCHGC010", None), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: ("MUCHGC0002", None), +} + + +PIPSELECT_SCHEMA = select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PipsolarSelect), + cv.Optional(CONF_OPTIONSMAP): ensure_option_map(), + cv.Optional(CONF_STATUSMAP): ensure_option_map(), + } +).extend(cv.COMPONENT_SCHEMA) + + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): PIPSELECT_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] + options_map = conf[CONF_OPTIONSMAP] + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await select.register_select(var, conf, options=list(options_map.keys())) + cg.add(getattr(paren, f"set_{type}_select")(var)) + cg.add(var.set_parent(paren)) + for mappingkey in options_map.keys(): + cg.add(var.add_mapping(mappingkey, options_map[mappingkey])) + if CONF_STATUSMAP in conf: + status_map = conf[CONF_STATUSMAP] + for mappingkey in status_map.keys(): + cg.add(var.add_status_mapping(mappingkey, status_map[mappingkey])) + # cg.add(var.set_optimistic(conf[CONF_OPTIMISTIC])) + + +# async def to_code(config): +# value_type = config[CONF_VALUE_TYPE] +# reg_count = config.get(CONF_REGISTER_COUNT) +# if reg_count is None: +# reg_count = TYPE_REGISTER_MAP[value_type] + +# options_map = config[CONF_OPTIONSMAP] + +# var = cg.new_Pvariable( +# config[CONF_ID], +# value_type, +# config[CONF_ADDRESS], +# reg_count, +# config[CONF_SKIP_UPDATES], +# config[CONF_FORCE_NEW_RANGE], +# list(options_map.values()), +# ) + +# await cg.register_component(var, config) +# await select.register_select(var, config, options=list(options_map.keys())) + +# parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) +# cg.add(parent.add_sensor_item(var)) +# cg.add(var.set_parent(parent)) +# cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) +# cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + +# if CONF_LAMBDA in config: +# template_ = await cg.process_lambda( +# config[CONF_LAMBDA], +# [ +# (ModbusSelect.operator("const_ptr"), "item"), +# (cg.int64, "x"), +# ( +# cg.std_vector.template(cg.uint8).operator("const").operator("ref"), +# "data", +# ), +# ], +# return_type=cg.optional.template(cg.std_string), +# ) +# cg.add(var.set_template(template_)) + +# if CONF_WRITE_LAMBDA in config: +# template_ = await cg.process_lambda( +# config[CONF_WRITE_LAMBDA], +# [ +# (ModbusSelect.operator("const_ptr"), "item"), +# (cg.std_string.operator("const").operator("ref"), "x"), +# (cg.int64, "value"), +# (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), +# ], +# return_type=cg.optional.template(cg.int64), +# ) +# cg.add(var.set_write_template(template_)) diff --git a/components/pipsolar/select/pipsolar_select.cpp b/components/pipsolar/select/pipsolar_select.cpp new file mode 100644 index 00000000..75e2ff45 --- /dev/null +++ b/components/pipsolar/select/pipsolar_select.cpp @@ -0,0 +1,42 @@ +#include "pipsolar_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.select"; + +void PipsolarSelect::dump_config() { LOG_SELECT(TAG, "Pipsolar Controller Select", this); } + +void PipsolarSelect::control(const std::string &value) { + ESP_LOGD(TAG, "got option: %s", value.c_str()); + if (this->mapping_.find(value) != this->mapping_.end()) { + ESP_LOGD(TAG, "found mapped option %s for option %s", this->mapping_[value].c_str(), value.c_str()); + this->parent_->switch_command(this->mapping_[value]); + } else { + ESP_LOGD(TAG, "could not find option %s in mapping", value.c_str()); + return; + } + // auto options = this->traits.get_options(); + // auto opt_it = std::find(options.cbegin(), options.cend(), value); + // size_t idx = std::distance(options.cbegin(), opt_it); + // std::string mapval = this->mapping_[idx]; + // ESP_LOGD(TAG, "Found value %s for option '%s'", *mapval.c_str(), value.c_str()); + + if (this->optimistic_) + this->publish_state(value); +} + +void PipsolarSelect::map_and_publish(std::string &value) { + ESP_LOGD(TAG, "got value: %s", value.c_str()); + if (this->status_mapping_.find(value) != this->status_mapping_.end()) { + ESP_LOGD(TAG, "found mapped option %s for option %s", this->status_mapping_[value].c_str(), value.c_str()); + this->publish_state(this->status_mapping_[value]); + } else { + ESP_LOGD(TAG, "could not find option %s in mapping", value.c_str()); + return; + } +} + +} // namespace pipsolar +} // namespace esphome diff --git a/components/pipsolar/select/pipsolar_select.h b/components/pipsolar/select/pipsolar_select.h new file mode 100644 index 00000000..06050579 --- /dev/null +++ b/components/pipsolar/select/pipsolar_select.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include "esphome/components/pipsolar/pipsolar.h" +#include "esphome/components/select/select.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; + +class PipsolarSelect : public Component, public select::Select { + public: + PipsolarSelect( + // uint16_t start_address, uint8_t register_count, uint8_t skip_updates, + // bool force_new_range, std::vector mapping + ) { + // this->mapping_ = std::move(mapping); + } + + void set_parent(Pipsolar *const parent) { this->parent_ = parent; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void add_mapping(std::string key, std::string value) { this->mapping_[key] = value; } + void add_status_mapping(std::string key, std::string value) { this->status_mapping_[key] = value; } + void dump_config() override; + // void parse_and_publish(const std::vector &data) override; + void control(const std::string &value) override; + void map_and_publish(std::string &value); + + protected: + std::map mapping_; + std::map status_mapping_; + + Pipsolar *parent_; + bool optimistic_{false}; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/components/pipsolar/sensor/__init__.py b/components/pipsolar/sensor/__init__.py new file mode 100644 index 00000000..9cd4ade6 --- /dev/null +++ b/components/pipsolar/sensor/__init__.py @@ -0,0 +1,306 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_BATTERY_VOLTAGE, + CONF_BUS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_WATT, +) + +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA + +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" + +# QPIGS sensors + +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_PV1_INPUT_CURRENT = "pv1_input_current" +CONF_PV1_INPUT_VOLTAGE = "pv1_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_PV1_CHARGING_POWER = "pv1_charging_power" + +# QPIGS2 sensors + +CONF_PV2_INPUT_CURRENT = "pv2_input_current" +CONF_PV2_INPUT_VOLTAGE = "pv2_input_voltage" +CONF_PV2_CHARGING_POWER = "pv2_charging_power" + + +TYPES = { + CONF_GRID_RATING_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_GRID_RATING_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + ), + CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + ), + CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_TYPE: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( + accuracy_decimals=1, + ), + CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_MACHINE_TYPE: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_TOPOLOGY: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_OUTPUT_MODE: sensor.sensor_schema( + accuracy_decimals=0, + ), + CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( + accuracy_decimals=1, + ), + CONF_PV_POWER_BALANCE: sensor.sensor_schema( + accuracy_decimals=1, + ), + CONF_GRID_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_GRID_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + ), + CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + ), + CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + ), + CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + ), + CONF_BUS_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + ), + CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_PV1_INPUT_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_PV1_INPUT_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_EEPROM_VERSION: sensor.sensor_schema( + accuracy_decimals=1, + ), + CONF_PV1_CHARGING_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + CONF_PV2_INPUT_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_PV2_INPUT_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_PV2_CHARGING_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=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/components/pipsolar/switch/__init__.py b/components/pipsolar/switch/__init__.py new file mode 100644 index 00000000..7658c7d4 --- /dev/null +++ b/components/pipsolar/switch/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import 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( + PipsolarSwitch, icon=ICON_POWER, block_inverted=True +).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 = await switch.new_switch(conf) + await cg.register_component(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/components/pipsolar/switch/pipsolar_switch.cpp b/components/pipsolar/switch/pipsolar_switch.cpp new file mode 100644 index 00000000..7eaeac1c --- /dev/null +++ b/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/components/pipsolar/switch/pipsolar_switch.h b/components/pipsolar/switch/pipsolar_switch.h new file mode 100644 index 00000000..11ff6c85 --- /dev/null +++ b/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(const std::string &command) { this->on_command_ = command; }; + void set_off_command(const std::string &command) { this->off_command_ = 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/components/pipsolar/text_sensor/__init__.py b/components/pipsolar/text_sensor/__init__.py new file mode 100644 index 00000000..afcae4b2 --- /dev/null +++ b/components/pipsolar/text_sensor/__init__.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor + +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA + +DEPENDENCIES = ["uart"] + +CONF_DEVICE_MODE = "device_mode" +CONF_LAST_QPIGS = "last_qpigs" +CONF_LAST_QPIGS2 = "last_qpigs2" +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" +CONF_LAST_QBATCD = "last_qbatcd" + +TYPES = [ + CONF_DEVICE_MODE, + CONF_LAST_QPIGS, + CONF_LAST_QPIGS2, + CONF_LAST_QPIRI, + CONF_LAST_QMOD, + CONF_LAST_QFLAG, + CONF_LAST_QPIWS, + CONF_LAST_QT, + CONF_LAST_QMN, + CONF_LAST_QBATCD, +# CONF_LAST_MUCHGC, +# CONF_LAST_MCHGC, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): text_sensor.text_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] + var = await text_sensor.new_text_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/components/pipsolar/text_sensor/pipsolar_textsensor.cpp b/components/pipsolar/text_sensor/pipsolar_textsensor.cpp new file mode 100644 index 00000000..ee1fe2d1 --- /dev/null +++ b/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/components/pipsolar/text_sensor/pipsolar_textsensor.h b/components/pipsolar/text_sensor/pipsolar_textsensor.h new file mode 100644 index 00000000..871f6d8d --- /dev/null +++ b/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/esp32-example.yaml b/esp32-example.yaml index 6b7d5a78..bbac0b7a 100644 --- a/esp32-example.yaml +++ b/esp32-example.yaml @@ -11,14 +11,26 @@ substitutions: esphome: name: ${name} - platform: ESP32 - board: esp-wrover-kit + min_version: 2024.6.0 + project: + name: "syssi.esphome-pipsolar@pip8084" + version: 1.0.0 + +esp32: + board: wemos_d1_mini32 + framework: + type: esp-idf + +external_components: + - source: github://syssi/esphome-pipsolar@pip8048 + refresh: 0s wifi: ssid: !secret wifi_ssid password: !secret wifi_password ota: + platform: esphome logger: baud_rate: 0 @@ -30,13 +42,13 @@ mqtt: id: mqtt_client uart: - - id: uart0 + - id: uart_0 baud_rate: 2400 tx_pin: GPIO1 rx_pin: GPIO3 pipsolar: - uart_id: uart0 + uart_id: uart_0 id: inverter0 sensor: @@ -119,10 +131,14 @@ sensor: name: "${name} battery_capacity_percent" inverter_heat_sink_temperature: name: "${name} inverter_heat_sink_temperature" - pv_input_current_for_battery: - name: "${name} pv_input_current_for_battery" - pv_input_voltage: - name: "${name} pv_input_voltage" + pv1_input_current: + name: "${name} pv1_input_current" + pv1_input_voltage: + name: "${name} pv1_input_voltage" + pv2_input_current: + name: "${name} pv2_input_current" + pv2_input_voltage: + name: "${name} pv2_input_voltage" battery_voltage_scc: name: "${name} battery_voltage_scc" battery_discharge_current: @@ -131,8 +147,10 @@ sensor: name: "${name} battery_voltage_offset_for_fans_on" # eeprom_version: # name: "${name} eeprom_version" - pv_charging_power: - name: "${name} pv_charging_power" + pv1_charging_power: + name: "${name} pv1_charging_power" + pv2_charging_power: + name: "${name} pv2_charging_power" text_sensor: - platform: pipsolar @@ -207,3 +225,126 @@ output: pipsolar_id: inverter0 battery_recharge_voltage: id: inverter0_battery_recharge_voltage_out + + - platform: pipsolar + pipsolar_id: inverter0 + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage_out + +select: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority: + id: inverter0_output_source_priority_select + name: inverter0_output_source_priority_select + optionsmap: + "Utility first": "POP00" + "Solar only": "POP01" + "Solar Battery Utility": "POP02" + statusmap: + "0": "Utility first" + "1": "Solar only" + "2": "Solar Battery Utility" + + - platform: pipsolar + pipsolar_id: inverter0 + charging_discharging_control: + # See MAX_Communication_Protocol_20200526.pdf page 18 + name: "${name} charging discharging control" + optionsmap: + "111": "PBATCD111" + "011": "PBATCD011" + "101": "PBATCD101" + "110": "PBATCD110" + "010": "PBATCD010" + "100": "PBATCD100" + "001": "PBATCD001" + "000": "PBATCD000" + statusmap: + "111": "111" + "011": "011" + "101": "101" + "110": "110" + "010": "010" + "100": "100" + "001": "001" + "000": "000" + + - platform: pipsolar + pipsolar_id: inverter0 + charger_source_priority: + name: ${name} charger_source_priority_select + optionsmap: +# "Utility first": "PCP00" + "Solar first": "PCP01" + "Solar and utility": "PCP02" + "Solar charging only": "PCP03" + statusmap: + # "0": "Utility first" + "1": "Solar first" + "2": "Solar and utility" + "3": "Solar charging only" + + - platform: pipsolar + pipsolar_id: inverter0 + current_max_ac_charging_current: + name: ${name} current_max_ac_charging_current_select + optionsmap: + "2A": "MUCHGC0002" + "10A": "MUCHGC0010" + "20A": "MUCHGC0020" + "30A": "MUCHGC0030" + "40A": "MUCHGC0040" + "50A": "MUCHGC0050" + "60A": "MUCHGC0060" + "70A": "MUCHGC0070" + "80A": "MUCHGC0080" + "90A": "MUCHGC0090" + "100A": "MUCHGC0100" + "110A": "MUCHGC0110" + "120A": "MUCHGC0120" + statusmap: + "2": "2A" + "10": "10A" + "20": "20A" + "30": "30A" + "40": "40A" + "50": "50A" + "60": "60A" + "70": "70A" + "80": "80A" + "90": "90A" + "100": "100A" + "110": "110A" + "120": "120A" + + - platform: pipsolar + pipsolar_id: inverter0 + current_max_charging_current: + name: ${name} current_max_charging_current_select + optionsmap: + "10A": "MCHGC010" + "20A": "MCHGC020" + "30A": "MCHGC030" + "40A": "MCHGC040" + "50A": "MCHGC050" + "60A": "MCHGC060" + "70A": "MCHGC070" + "80A": "MCHGC080" + "90A": "MCHGC090" + "100A": "MCHGC100" + "110A": "MCHGC110" + "120A": "MCHGC120" + statusmap: + "10": "10A" + "20": "20A" + "30": "30A" + "40": "40A" + "50": "50A" + "60": "60A" + "70": "70A" + "80": "80A" + "90": "90A" + "100": "100A" + "110": "110A" + "120": "120A" diff --git a/esp8266-example.yaml b/esp8266-example.yaml index ee6134aa..619813ee 100644 --- a/esp8266-example.yaml +++ b/esp8266-example.yaml @@ -11,14 +11,24 @@ substitutions: esphome: name: ${name} - platform: ESP8266 + min_version: 2024.6.0 + project: + name: "syssi.esphome-pipsolar@pip8048" + version: 1.0.0 + +esp8266: board: d1_mini +external_components: + - source: github://syssi/esphome-pipsolar@pip8048 + refresh: 0s + wifi: ssid: !secret wifi_ssid password: !secret wifi_password ota: + platform: esphome logger: baud_rate: 0 @@ -30,13 +40,13 @@ mqtt: id: mqtt_client uart: - id: uart0 + id: uart_0 baud_rate: 2400 tx_pin: GPIO1 rx_pin: GPIO3 pipsolar: - uart_id: uart0 + uart_id: uart_0 id: inverter0 sensor: @@ -119,10 +129,14 @@ sensor: name: "${name} battery_capacity_percent" inverter_heat_sink_temperature: name: "${name} inverter_heat_sink_temperature" - pv_input_current_for_battery: - name: "${name} pv_input_current_for_battery" - pv_input_voltage: - name: "${name} pv_input_voltage" + pv1_input_current: + name: "${name} pv1_input_current" + pv1_input_voltage: + name: "${name} pv1_input_voltage" + pv2_input_current: + name: "${name} pv2_input_current" + pv2_input_voltage: + name: "${name} pv2_input_voltage" battery_voltage_scc: name: "${name} battery_voltage_scc" battery_discharge_current: @@ -131,8 +145,10 @@ sensor: name: "${name} battery_voltage_offset_for_fans_on" # eeprom_version: # name: "${name} eeprom_version" - pv_charging_power: - name: "${name} pv_charging_power" + pv1_charging_power: + name: "${name} pv1_charging_power" + pv2_charging_power: + name: "${name} pv2_charging_power" text_sensor: - platform: pipsolar @@ -207,3 +223,126 @@ output: pipsolar_id: inverter0 battery_recharge_voltage: id: inverter0_battery_recharge_voltage_out + + - platform: pipsolar + pipsolar_id: inverter0 + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage_out + +select: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority: + id: inverter0_output_source_priority_select + name: inverter0_output_source_priority_select + optionsmap: + "Utility first": "POP00" + "Solar only": "POP01" + "Solar Battery Utility": "POP02" + statusmap: + "0": "Utility first" + "1": "Solar only" + "2": "Solar Battery Utility" + + - platform: pipsolar + pipsolar_id: inverter0 + charging_discharging_control: + # See MAX_Communication_Protocol_20200526.pdf page 18 + name: "${name} charging discharging control" + optionsmap: + "111": "PBATCD111" + "011": "PBATCD011" + "101": "PBATCD101" + "110": "PBATCD110" + "010": "PBATCD010" + "100": "PBATCD100" + "001": "PBATCD001" + "000": "PBATCD000" + statusmap: + "111": "111" + "011": "011" + "101": "101" + "110": "110" + "010": "010" + "100": "100" + "001": "001" + "000": "000" + + - platform: pipsolar + pipsolar_id: inverter0 + charger_source_priority: + name: ${name} charger_source_priority_select + optionsmap: +# "Utility first": "PCP00" + "Solar first": "PCP01" + "Solar and utility": "PCP02" + "Solar charging only": "PCP03" + statusmap: + # "0": "Utility first" + "1": "Solar first" + "2": "Solar and utility" + "3": "Solar charging only" + + - platform: pipsolar + pipsolar_id: inverter0 + current_max_ac_charging_current: + name: ${name} current_max_ac_charging_current_select + optionsmap: + "2A": "MUCHGC0002" + "10A": "MUCHGC0010" + "20A": "MUCHGC0020" + "30A": "MUCHGC0030" + "40A": "MUCHGC0040" + "50A": "MUCHGC0050" + "60A": "MUCHGC0060" + "70A": "MUCHGC0070" + "80A": "MUCHGC0080" + "90A": "MUCHGC0090" + "100A": "MUCHGC0100" + "110A": "MUCHGC0110" + "120A": "MUCHGC0120" + statusmap: + "2": "2A" + "10": "10A" + "20": "20A" + "30": "30A" + "40": "40A" + "50": "50A" + "60": "60A" + "70": "70A" + "80": "80A" + "90": "90A" + "100": "100A" + "110": "110A" + "120": "120A" + + - platform: pipsolar + pipsolar_id: inverter0 + current_max_charging_current: + name: ${name} current_max_charging_current_select + optionsmap: + "10A": "MCHGC010" + "20A": "MCHGC020" + "30A": "MCHGC030" + "40A": "MCHGC040" + "50A": "MCHGC050" + "60A": "MCHGC060" + "70A": "MCHGC070" + "80A": "MCHGC080" + "90A": "MCHGC090" + "100A": "MCHGC100" + "110A": "MCHGC110" + "120A": "MCHGC120" + statusmap: + "10": "10A" + "20": "20A" + "30": "30A" + "40": "40A" + "50": "50A" + "60": "60A" + "70": "70A" + "80": "80A" + "90": "90A" + "100": "100A" + "110": "110A" + "120": "120A" diff --git a/tests/esp8266-test-ping.yaml b/tests/esp8266-test-ping.yaml new file mode 100644 index 00000000..324bd1b7 --- /dev/null +++ b/tests/esp8266-test-ping.yaml @@ -0,0 +1,41 @@ +substitutions: + name: pipsolar + tx_pin: GPIO4 + rx_pin: GPIO5 + +esphome: + name: ${name} + +esp8266: + board: d1_mini + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +ota: + platform: esphome + +logger: + level: DEBUG + +api: + reboot_timeout: 0s + +uart: + id: uart_0 + baud_rate: 2400 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + debug: + direction: BOTH + dummy_receiver: true + after: + delimiter: "\r" + sequence: + - lambda: UARTDebug::log_string(direction, bytes); + +interval: + - interval: 2s + then: + - uart.write: "ping\r" diff --git a/tests/esp8266-test-pong.yaml b/tests/esp8266-test-pong.yaml new file mode 100644 index 00000000..ff10a08f --- /dev/null +++ b/tests/esp8266-test-pong.yaml @@ -0,0 +1,41 @@ +substitutions: + name: pipsolar + tx_pin: GPIO4 + rx_pin: GPIO5 + +esphome: + name: ${name} + +esp8266: + board: d1_mini + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +ota: + platform: esphome + +logger: + level: DEBUG + +api: + reboot_timeout: 0s + +uart: + id: uart_0 + baud_rate: 2400 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + debug: + direction: BOTH + dummy_receiver: true + after: + delimiter: "\r" + sequence: + - lambda: UARTDebug::log_string(direction, bytes); + +interval: + - interval: 2s + then: + - uart.write: "pong\r" diff --git a/tests/esp8266-test-protocols.yaml b/tests/esp8266-test-protocols.yaml new file mode 100644 index 00000000..c36e559c --- /dev/null +++ b/tests/esp8266-test-protocols.yaml @@ -0,0 +1,123 @@ +substitutions: + name: pipsolar + tx_pin: GPIO4 + rx_pin: GPIO5 + +esphome: + name: ${name} + +esp8266: + board: d1_mini + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +ota: + platform: esphome + +logger: + level: DEBUG + +api: + reboot_timeout: 0s + +uart: + id: uart_0 + baud_rate: 2400 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + debug: + direction: BOTH + dummy_receiver: true + after: + delimiter: "\r" + sequence: + - lambda: UARTDebug::log_string(direction, bytes); + +interval: + - interval: 80s + then: + # PI30/PI30MAX/PI30REVO/PI41 + - logger.log: + level: INFO + format: "Testing PI30/PI30MAX/PI30REVO/PI41 commands..." + - logger.log: + level: INFO + format: "This is the set of commands supported by the pipsolar component!" + - uart.write: [0x51, 0x50, 0x49, 0xBE, 0xAC, 0x0D] # QPI\xbe\xac\r + - delay: 1s + - uart.write: [0x51, 0x44, 0x49, 0x71, 0x1B, 0x0D] # QDIq\x1b\r + - delay: 1s + - uart.write: [0x51, 0x46, 0x4C, 0x41, 0x47, 0x98, 0x74, 0x0D] # QFLAG\x98t\r + - delay: 1s + - uart.write: [0x51, 0x4D, 0x4E, 0xBB, 0x64, 0x0D] # QMN\xbbd\r + - delay: 1s + - uart.write: [0x51, 0x4D, 0x4F, 0x44, 0x49, 0xC1, 0x0D] # QMODI\xc1\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x49, 0x47, 0x53, 0xB7, 0xA9, 0x0D] # QPIGS\xB7\xA9\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x49, 0x52, 0x49, 0xF8, 0x54, 0x0D] # QPIRI\xF8T\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x49, 0x57, 0x53, 0xB4, 0xDA, 0x0D] # QPIWS\xb4\xda\r + - delay: 1s + - uart.write: [0x51, 0x54, 0x27, 0xFF, 0x0D] # QT'\xff\r + - delay: 1s + # Test some PI30 commands from Phocos Any-Grid PSW-H inverters + - uart.write: [0x51, 0x50, 0x47, 0x53, 0x30, 0x3F, 0xDA, 0x0D] # QPGS0\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x47, 0x53, 0x31, 0x2F, 0xFB, 0x0D] # QPGS1\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x47, 0x53, 0x32, 0x1F, 0x98, 0x0D] # QPGS2\r + - delay: 1s + + # PI41 split phase + - logger.log: + level: INFO + format: "Testing PI41 split phase / multiple strings commands..." + - uart.write: [0x51, 0x50, 0x49, 0x47, 0x53, 0x32, 0x68, 0x2D, 0x0D] # QPIGS2h-\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x32, 0x47, 0x53, 0x30, 0x14, 0x05, 0x0D] # QP2GS0\x14\x05\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x32, 0x47, 0x53, 0x31, 0x04, 0x24, 0x0D] # QP2GS1\x04$\r + - delay: 1s + + # PI18 + - logger.log: + level: INFO + format: "Testing unsupported PI18 commands..." + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x35, 0x50, 0x49, 0x71, 0x8B, 0x0D] # ^P005PIq\x8b\r + - delay: 1s + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x35, 0x47, 0x53, 0x58, 0x14, 0x0D] # ^P005GSX\x14\r + - delay: 1s + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x36, 0x4D, 0x4F, 0x44, 0xDD, 0xBE, 0x0D] # ^P006MOD\xdd\xbe\r + - delay: 1s + + # PI17 + - logger.log: + level: INFO + format: "Testing unsupported PI17 commands..." + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x33, 0x50, 0x49, 0x0D] # ^P003PI\r + - delay: 1s + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x34, 0x4D, 0x4F, 0x44, 0x0D] # ^P004MOD\r + - delay: 1s + - uart.write: [0x5E, 0x50, 0x30, 0x30, 0x35, 0x46, 0x4C, 0x41, 0x47, 0x0D] # ^P005FLAG\r + - delay: 1s + + # PI16 + - logger.log: + level: INFO + format: "Testing unsupported PI16 commands..." + - uart.write: [0x51, 0x50, 0x49, 0x0D] # QPI\r + - delay: 1s + - uart.write: [0x51, 0x4D, 0x4F, 0x44, 0x0D] # QMOD\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x49, 0x47, 0x53, 0x0D] # QPIGS\r + - delay: 1s + - uart.write: [0x51, 0x50, 0x49, 0x52, 0x49, 0x0D] # QPIRI\r + - delay: 1s + - uart.write: [0x51, 0x4D, 0x4F, 0x44, 0x0D] # QMOD\r + + - logger.log: + level: INFO + format: "Done. Repeating..."