diff --git a/README.md b/README.md index d89d711..c41c644 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,143 @@ # esphome-apc-ups -![GitHub actions](https://github.com/syssi/esphome-apc-ups/actions/workflows/ci.yaml/badge.svg) -![GitHub stars](https://img.shields.io/github/stars/syssi/esphome-apc-ups) -![GitHub forks](https://img.shields.io/github/forks/syssi/esphome-apc-ups) -![GitHub watchers](https://img.shields.io/github/watchers/syssi/esphome-apc-ups) -[!["Buy Me A Coffee"](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://www.buymeacoffee.com/syssi) +ESPHome component to monitor and control a APC UPS via RS232 with MQTT -ESPHome component to monitor and control a APC UPS via RS232 +* Fixed switches. They're working now. +* When power is lost or restored, an external command (Q-status flag) is sent to update the status (on_line and on_battery) +* Added a record of the date of battery replacement +* Added switches: shutdown with grace period, soft shutdown, shutdown immediately, turn on -## Supported devices +## Tested on the device -* APC SU420INET (firmware `21.3.I`) -* APC SUVS420I (firmware `42.L.I`) * APC SUA1000I (firmware `652.13.I`) ## Requirements -* [ESPHome 2024.6.0 or higher](https://github.com/esphome/esphome/releases). +* [ESPHome 2024.5.0 or higher](https://github.com/esphome/esphome/releases). * Generic ESP32/ESP8266 board ## Schematics ``` RS232 UART-TTL -┌───────────┐ ┌──────────┐ ┌─────────┐ -│ │ │ │<----- RX ----->│ │ -│ │<---- TX ---->│ RS232 │<----- TX ----->│ ESP32/ │ -│ APC UPS │<---- RX ---->│ to TTL │<----- GND ---->│ ESP8266 │ -│ │<---- GND --->│ module │<-- 3.3V VCC -->│ │<--- VCC -│ │ │ │ │ │<--- GND -└───────────┘ └──────────┘ └─────────┘ +┌───────────┐ ┌──────────┐ ┌─────────┐ +│ │<--- TX -----│ RS232 │<---- TX ------│ │ +│ │---- RX ---->│ to TTL │----- RX ----->│ ESP32/ │ +│ APC UPS │<--- GND --->│ module │<---- GND ---->│ ESP8266 │ +│ │ | |<-- 3.3V VCC --│ │ +│ │ └──────────┘ │ │ +│ │ ┌──────────┐ │ │ +│ │<--- GND --->│ DC-DC │<---- GND ---->│ │ +│ │---- 24V --->| XL1509 │-- 3.3V VCC -->│ │ +└───────────┘ └──────────┘ └─────────┘ ``` ### D-SUB 9P connector -| Pin | Purpose | MAX3232 pin | -|:---:| :----------- | :---------------- | -| 1 | RX | P14 (DOUT1) | -| 2 | TX | P13 (RIN1) | -| 3 | | | -| 4 | | | -| 5 | | | -| 6 | | | -| 7 | | | -| 8 | | | -| 9 | GND | P15 (GND) | +| UPS pin | RS232 to TTL pin | MAX3232 pin | +| :----------------- | :--------------- | :----------------- | +| RX (pin 1) | RX (pin 2) | P14 (DOUT1) | +| TX (pin 2) | TX (pin 3) | P13 (RIN1) | +| GND (pin 4, 9) | GND (pin 5) | P15 (GND) | +| 24V battery (pin8) | 24V (free pin 9) | connected to DC-DC | ### MAX3232 -| Pin | Label | ESPHome | ESP8266 example | ESP32 example | -| :----------- | :----------- | :---------- | :--------------- | :------------ | -| P11 (DIN1) | TXD | `tx_pin` | `GPIO4` | `GPIO16` | -| P12 (ROUT1) | RXD | `rx_pin` | `GPIO5` | `GPIO17` | -| P16 (VCC) | VCC | | | | -| P15 (GND) | GND | | | | +| Pin | Label | ESPHome | ESP8266 | ESP32 | ESP-01 | +| :----------- | :----------- | :---------- | :------ | :------- | :--- | +| P11 (DIN1) | TXD | `tx_pin` | `GPIO4` | `GPIO16` | `GPIO1` | +| P12 (ROUT1) | RXD | `rx_pin` | `GPIO5` | `GPIO17` | `GPIO3` | +| P16 (VCC) | VCC | | | | | +| P15 (GND) | GND | | | | | ## Installation You can install this component with [ESPHome external components feature](https://esphome.io/components/external_components.html) like this: ```yaml external_components: - - source: github://syssi/esphome-apc-ups@main + - source: github://samoswall/esphome-apc-ups@main ``` -or just use the `esp32-example.yaml` as proof of concept: - -```bash -# Install esphome -pip3 install esphome - -# Clone this external component -git clone https://github.com/syssi/esphome-apc-ups.git -cd esphome-apc-ups - -# Create a secrets.yaml containing some setup specific secrets -cat > secrets.yaml <state_ = STATE_IDLE; this->command_start_millis_ = 0; this->add_polling_command_("Y", POLLING_Y); + this->publish_state_(this->last_battery_change_new_date_, "mm/dd/yy"); // Initial value } void ApcUps::empty_uart_buffer_() { @@ -17,12 +18,12 @@ void ApcUps::empty_uart_buffer_() { while (this->available()) { this->read_byte(&byte); // 0x21 symbol ! - if (byte == 0x21) { + if (byte==0x21) { ESP_LOGI(TAG, "Power failure"); queue_command_("Q", 1); } // 0x24 symbol $ - if (byte == 0x24) { + if (byte==0x24) { ESP_LOGI(TAG, "Power has been restored"); queue_command_("Q", 1); } @@ -49,7 +50,7 @@ void ApcUps::loop() { } } if (this->state_ == STATE_COMMAND_COMPLETE) { - if (this->read_pos_ > 0) { + if (this->read_pos_ > 0 || multibyte_command_ > 0) { // for log ESP_LOGI(TAG, "Command successful"); } else { ESP_LOGE(TAG, "Command not successful"); @@ -80,7 +81,66 @@ void ApcUps::loop() { this->value_self_test_ = (this->read_buffer_[0] == 'O' && this->read_buffer_[1] == 'K'); this->publish_state_(this->self_test_, !value_self_test_); break; + case 0x78: + // "x" Start multibyte command save new date + ESP_LOGD(TAG, "Send multibyte command save new date"); + multibyte_command_ = 1; + break; + case 0x4B: + // "K" Shutdown with grace period + ESP_LOGD(TAG, "Send Shutdown with grace period"); + multibyte_command_ = 2; + break; + case 0x53: + // "S" Soft shutdown + ESP_LOGD(TAG, "Send Soft shutdown"); + multibyte_command_ = 3; + break; + case 0x5A: + // "Z" Shutdown immediately + ESP_LOGD(TAG, "Send Shutdown immediately"); + multibyte_command_ = 4; + break; + case 0x0E: + // "Ctrl_N" Turn ON UPS + ESP_LOGD(TAG, "Send Turn ON"); + multibyte_command_ = 5; + break; } + + if (multibyte_command_ > 0) { // Waiting for the end of the multibyte command + if (this->read_buffer_[0] == 'O' && this->read_buffer_[1] == 'K') { + switch (multibyte_command_) { + case 1: + this->publish_state_(this->save_last_battery_change_date_, false); + break; + case 2: + this->publish_state_(this->shutdown_with_grace_period_, false); + break; + case 3: + this->publish_state_(this->soft_shutdown_, false); + break; + } + multibyte_command_ = 0; + } + if (this->read_buffer_[0] == 'N' && this->read_buffer_[1] == 'A' && multibyte_command_ == 3) { // Command "S" Only works when on battery + this->publish_state_(this->soft_shutdown_, false); + multibyte_command_ = 0; + } + if (this->read_buffer_[0] == 'N' && this->read_buffer_[1] == 'A' && multibyte_command_ == 5) { // It will not turn on when running on battery power + this->publish_state_(this->turn_on_, false); + multibyte_command_ = 0; + } + if (this->command_queue_[this->command_queue_position_ +1][0] != 0x5A && multibyte_command_ == 4) { + this->publish_state_(this->shutdown_immediately_, false); + multibyte_command_ = 0; + } + if (this->read_buffer_[0] == '?' && multibyte_command_ == 5) { + this->publish_state_(this->turn_on_, false); + multibyte_command_ = 0; + } + } + this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; @@ -130,9 +190,9 @@ void ApcUps::loop() { this->publish_state_(this->battery_low_, check_bit_(value_status_bitmask_, 64)); this->publish_state_(this->replace_battery_, check_bit_(value_status_bitmask_, 128)); break; - case POLLING_X: + case POLLING_X: this->publish_state_(this->self_test_results_, value_self_test_results_); - break; + break; case POLLING_V: this->publish_state_(this->old_firmware_version_, value_old_firmware_version_); break; @@ -153,7 +213,7 @@ void ApcUps::loop() { break; case POLLING_LOWER_G: this->publish_state_(this->nominal_battery_voltage_, value_nominal_battery_voltage_); - break; + break; case POLLING_LOWER_H: this->publish_state_(this->ambient_humidity_, value_ambient_humidity_); break; @@ -162,7 +222,7 @@ void ApcUps::loop() { break; case POLLING_LOWER_K: this->publish_state_(this->alarm_delay_, value_alarm_delay_); - break; + break; case POLLING_LOWER_L: this->publish_state_(this->low_transfer_voltage_, value_low_transfer_voltage_); break; @@ -189,13 +249,13 @@ void ApcUps::loop() { break; case POLLING_LOWER_Y: this->publish_state_(this->alarm_delay_, value_alarm_delay_); - break; + break; case POLLING_9: this->publish_state_(this->line_quality_, value_line_quality_); break; case POLLING_CTRL_A: this->publish_state_(this->model_name_, value_model_name_); - break; + break; default: ESP_LOGD(TAG, "Response not implemented"); break; @@ -216,19 +276,19 @@ void ApcUps::loop() { case POLLING_B: ESP_LOGD(TAG, "Decode B"); // "13.61\r\n" - sscanf(tmp, "%f", &value_battery_voltage_); // NOLINT + sscanf(tmp, "%f", &value_battery_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_C: ESP_LOGD(TAG, "Decode C"); // "36\r\n" - sscanf(tmp, "%f", &value_internal_temperature_); // NOLINT + sscanf(tmp, "%f", &value_internal_temperature_); this->state_ = STATE_POLL_DECODED; break; case POLLING_F: ESP_LOGD(TAG, "Decode F"); // "50.00\r\n" - sscanf(tmp, "%f", &value_grid_frequency_); // NOLINT + sscanf(tmp, "%f", &value_grid_frequency_); this->state_ = STATE_POLL_DECODED; break; case POLLING_G: @@ -240,31 +300,31 @@ void ApcUps::loop() { case POLLING_L: ESP_LOGD(TAG, "Decode L"); // "231.8\r\n" - sscanf(tmp, "%f", &value_grid_voltage_); // NOLINT + sscanf(tmp, "%f", &value_grid_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_M: ESP_LOGD(TAG, "Decode M"); // "231.8\r\n" - sscanf(tmp, "%f", &value_max_grid_voltage_); // NOLINT + sscanf(tmp, "%f", &value_max_grid_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_N: ESP_LOGD(TAG, "Decode N"); // "231.8\r\n" - sscanf(tmp, "%f", &value_min_grid_voltage_); // NOLINT + sscanf(tmp, "%f", &value_min_grid_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_O: ESP_LOGD(TAG, "Decode O"); // "231.8\r\n" - sscanf(tmp, "%f", &value_ac_output_voltage_); // NOLINT + sscanf(tmp, "%f", &value_ac_output_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_P: ESP_LOGD(TAG, "Decode P"); // "009.1\r\n" - sscanf(tmp, "%f", &value_ac_output_load_); // NOLINT + sscanf(tmp, "%f", &value_ac_output_load_); this->state_ = STATE_POLL_DECODED; break; case POLLING_V: @@ -272,11 +332,11 @@ void ApcUps::loop() { // "FWI\r\n" this->value_old_firmware_version_ = tmp; this->state_ = STATE_POLL_DECODED; - break; + break; case POLLING_Q: ESP_LOGD(TAG, "Decode Q"); // "08\r\n" - sscanf(tmp, "%x", &value_status_bitmask_); // NOLINT + sscanf(tmp, "%x", &value_status_bitmask_); this->state_ = STATE_POLL_DECODED; break; case POLLING_X: @@ -306,31 +366,31 @@ void ApcUps::loop() { case POLLING_LOWER_E: ESP_LOGD(TAG, "Decode e"); // "53.8\r\n" - sscanf(tmp, "%f", &value_return_threshold_); // NOLINT + sscanf(tmp, "%f", &value_return_threshold_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_F: ESP_LOGD(TAG, "Decode f"); // "100.0\r\n" - sscanf(tmp, "%f", &value_state_of_charge_); // NOLINT + sscanf(tmp, "%f", &value_state_of_charge_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_G: ESP_LOGD(TAG, "Decode g"); // "53.8\r\n" - sscanf(tmp, "%f", &value_nominal_battery_voltage_); // NOLINT + sscanf(tmp, "%f", &value_nominal_battery_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_H: ESP_LOGD(TAG, "Decode h"); // "100.0\r\n" - sscanf(tmp, "%f", &value_ambient_humidity_); // NOLINT + sscanf(tmp, "%f", &value_ambient_humidity_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_J: ESP_LOGD(TAG, "Decode j"); // "0042:\r\n" - sscanf(tmp, "%f:", &value_estimated_runtime_); // NOLINT + sscanf(tmp, "%f:", &value_estimated_runtime_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_K: @@ -342,7 +402,7 @@ void ApcUps::loop() { case POLLING_LOWER_L: ESP_LOGD(TAG, "Decode l"); // "80.5\r\n" - sscanf(tmp, "%f", &value_low_transfer_voltage_); // NOLINT + sscanf(tmp, "%f", &value_low_transfer_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_M: @@ -360,21 +420,21 @@ void ApcUps::loop() { case POLLING_LOWER_O: ESP_LOGD(TAG, "Decode o"); // "80.5\r\n" - sscanf(tmp, "%f", &value_nominal_output_voltage_); // NOLINT + sscanf(tmp, "%f", &value_nominal_output_voltage_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_T: ESP_LOGD(TAG, "Decode t"); // "80.5\r\n" - sscanf(tmp, "%f", &value_ambient_temperature_); // NOLINT + sscanf(tmp, "%f", &value_ambient_temperature_); this->state_ = STATE_POLL_DECODED; break; case POLLING_LOWER_U: ESP_LOGD(TAG, "Decode u"); // "80.5\r\n" - sscanf(tmp, "%f", &value_upper_transfer_voltage_); // NOLINT + sscanf(tmp, "%f", &value_upper_transfer_voltage_); this->state_ = STATE_POLL_DECODED; - break; + break; case POLLING_LOWER_V: ESP_LOGD(TAG, "Decode v"); // alarm @@ -392,7 +452,7 @@ void ApcUps::loop() { // copyright_notice this->value_copyright_notice_ = tmp; this->state_ = STATE_POLL_DECODED; - break; + break; case POLLING_9: ESP_LOGD(TAG, "Decode 9"); // line_quality @@ -462,6 +522,7 @@ void ApcUps::loop() { } } + uint8_t ApcUps::check_incoming_length_(uint8_t length) { if (this->read_pos_ - 3 == length) { return 1; @@ -526,9 +587,47 @@ void ApcUps::queue_command_(const char *command, uint8_t length) { } void ApcUps::switch_command(const std::string &command) { - ESP_LOGD(TAG, "got command: %s", command.c_str()); - queue_command_(command.c_str(), command.length()); + if (command=="SDATE") { // Send multibyte command Save last battery change date + ESP_LOGD(TAG, "got command to save date"); + queue_command_("x", 1); + char tmpd[10]; + sprintf(tmpd, "%s", this->last_battery_change_new_date_->state.c_str()); + char tmpd1[2]= { '-', '\0' }; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[0]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[1]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[2]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[3]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[4]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[5]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[6]; + queue_command_(tmpd1, 1); + tmpd1[0] = tmpd[7]; + queue_command_(tmpd1, 1); + } else { // Send one byte command + ESP_LOGD(TAG, "got command: %s", command.c_str()); + if (command=="K" || command=="Z" || command=="CTRL_N") { // Send two bytes command + if (command=="CTRL_N") { + char tmpd2[2]= { '\x0E', '\0' }; + queue_command_(tmpd2, 1); + queue_command_(tmpd2, 1); + } else { + queue_command_(command.c_str(), 1); + queue_command_(command.c_str(), 1); + } + } else { + queue_command_(command.c_str(), 1); + } + } } + + void ApcUps::dump_config() { ESP_LOGCONFIG(TAG, "ApcUps:"); ESP_LOGCONFIG(TAG, "used commands:"); @@ -549,14 +648,16 @@ void ApcUps::add_polling_command_(const char *command, ENUMPollingCommand pollin } } if (used_polling_command.length == 0) { - if (strcmp(command, "A") == 0 || strcmp(command, "D") == 0 || strcmp(command, "U") == 0 || - strcmp(command, "W") == 0) { - return; - }; // Exclusion from the command queue + if (strcmp(command, "A") == 0 || strcmp(command, "D") == 0 || strcmp(command, "U") == 0 || strcmp(command, "W") == 0 || + strcmp(command, "K") == 0 || strcmp(command, "S") == 0 || strcmp(command, "Z") == 0 || strcmp(command, "CTRL_N") == 0 || + strcmp(command, "SDATE") == 0 || strcmp(command, "DATE") == 0) {return;}; // Exclusion from the command queue + if (strcmp(command, "CTRL_A") == 0){ + command = command_ctrl_a; + } 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) + used_polling_command.command = new uint8_t[length]; size_t i = 0; for (; beg != end; ++beg, ++i) { used_polling_command.command[i] = (uint8_t) (*beg); @@ -597,5 +698,9 @@ void ApcUps::publish_state_(text_sensor::TextSensor *text_sensor, const std::str text_sensor->publish_state(state); } +void ApcUps::set_last_battery_change_new_date(std::string value) { + this->publish_state_(this->last_battery_change_new_date_, value.substr(0,8)); +} + } // namespace apc_ups } // namespace esphome diff --git a/components/apc_ups/apc_ups.h b/components/apc_ups/apc_ups.h index 419e44c..1bc55c2 100644 --- a/components/apc_ups/apc_ups.h +++ b/components/apc_ups/apc_ups.h @@ -22,21 +22,21 @@ enum ENUMPollingCommand { POLLING_G = 7, // Cause of last transfer to battery POLLING_I = 8, // Measure-UPS alarm enable @TODO POLLING_J = 9, // Measure-UPS alarm status @TODO - POLLING_K = 10, // Shutdown with grace period (no return) @TODO + POLLING_K = 10, // Shutdown with grace period (no return) ---------------------------- @NEW POLLING_L = 11, // Input line voltage POLLING_M = 12, // Maximum line voltage POLLING_N = 13, // Minimum line voltage POLLING_O = 14, // Output voltage POLLING_P = 15, // Power load in % POLLING_Q = 16, // Status flags - POLLING_R = 17, // Turn dumb - POLLING_S = 18, // Soft shutdown @TODO + POLLING_R = 17, // Turn dumb @TODO + POLLING_S = 18, // Soft shutdown ----------------------------------------------------- @NEW POLLING_U = 19, // Simulate power failure POLLING_V = 20, // Old firmware version POLLING_W = 21, // Self test POLLING_X = 22, // Self test results - POLLING_Z = 23, // Shutdown immediately @TODO - POLLING_LOWER_A = 24, // Protocol info + POLLING_Z = 23, // Shutdown immediately ---------------------------------------------- @NEW + POLLING_LOWER_A = 24, // Protocol info @TODO POLLING_LOWER_B = 25, // Firmware revision POLLING_LOWER_C = 26, // UPS local identifier POLLING_LOWER_E = 27, // Return threshold @@ -57,11 +57,14 @@ enum ENUMPollingCommand { POLLING_LOWER_T = 42, // Measure-UPS ambient temperature POLLING_LOWER_U = 43, // Upper transfer voltage POLLING_LOWER_V = 44, // Measure-UPS firmware - POLLING_LOWER_X = 45, // Last battery change date + POLLING_LOWER_X = 45, // Last battery change date ------------------------------------------ @NEW POLLING_LOWER_Y = 46, // Copyright notice POLLING_LOWER_Z = 47, // Reset to factory settings @TODO POLLING_9 = 48, // Line quality - POLLING_CTRL_A = 49, // Model name @TODO + POLLING_CTRL_A = 49, // Model name + POLLING_DATE = 50, // New battery change date before saving ----------------------------- @NEW + POLLING_SDATE = 51, // Save last battery change date ------------------------------------- @NEW + POLLING_CTRL_N = 52, // Turn ON UPS ------------------------------------------------------- @NEW }; struct PollingCommand { uint8_t *command; @@ -83,11 +86,13 @@ struct PollingCommand { void set_##name(type *name) { /* NOLINT */ \ this->name##_ = name; \ this->add_polling_command_(#polling_command, POLLING_##ident); \ - } + } \ + #define APC_UPS_SENSOR(name, ident, polling_command, value_type) \ APC_UPS_VALUED_ENTITY_(sensor::Sensor, name, ident, polling_command, value_type) -#define APC_UPS_SWITCH(name, ident, polling_command) APC_UPS_ENTITY_(switch_::Switch, name, ident, polling_command) +#define APC_UPS_SWITCH(name, ident, polling_command) \ + APC_UPS_ENTITY_(switch_::Switch, name, ident, polling_command) #define APC_UPS_VALUED_SWITCH(name, ident, polling_command, value_type) \ APC_UPS_VALUED_ENTITY_(switch_::Switch, name, ident, polling_command, value_type) #define APC_UPS_VALUED_BINARY_SENSOR(name, ident, polling_command, value_type) \ @@ -96,7 +101,7 @@ struct PollingCommand { APC_UPS_ENTITY_(binary_sensor::BinarySensor, name, ident, polling_command) #define APC_UPS_VALUED_TEXT_SENSOR(name, ident, polling_command, value_type) \ APC_UPS_VALUED_ENTITY_(text_sensor::TextSensor, name, ident, polling_command, value_type) -#define APC_UPS_TEXT_SENSOR(name, polling_command) \ +#define APC_UPS_TEXT_SENSOR(name, ident, polling_command) \ APC_UPS_ENTITY_(text_sensor::TextSensor, name, ident, polling_command) class ApcUps : public uart::UARTDevice, public PollingComponent { @@ -133,31 +138,39 @@ class ApcUps : public uart::UARTDevice, public PollingComponent { APC_UPS_VALUED_TEXT_SENSOR(protocol_info, LOWER_A, a, std::string) APC_UPS_VALUED_TEXT_SENSOR(firmware_revision, LOWER_B, b, std::string) APC_UPS_VALUED_TEXT_SENSOR(local_identifier, LOWER_C, c, std::string) - APC_UPS_VALUED_TEXT_SENSOR(alarm_delay, LOWER_K, k, std::string) + APC_UPS_VALUED_TEXT_SENSOR(alarm_delay, LOWER_K, k , std::string) APC_UPS_VALUED_TEXT_SENSOR(manufacture_date, LOWER_M, m, std::string) APC_UPS_VALUED_TEXT_SENSOR(serial_number, LOWER_N, n, std::string) APC_UPS_SENSOR(ambient_temperature, LOWER_T, t, float) - APC_UPS_VALUED_TEXT_SENSOR(measure_upc_firmware, LOWER_V, v, std::string) + APC_UPS_VALUED_TEXT_SENSOR(measure_upc_firmware, LOWER_V, v , std::string) APC_UPS_VALUED_TEXT_SENSOR(last_battery_change_date, LOWER_X, x, std::string) APC_UPS_VALUED_TEXT_SENSOR(copyright_notice, LOWER_Y, Y, std::string) - APC_UPS_VALUED_TEXT_SENSOR(line_quality, 9, 9, std::string) - APC_UPS_VALUED_TEXT_SENSOR(model_name, CTRL_A, "\x01", std::string) - + APC_UPS_VALUED_TEXT_SENSOR(line_quality, 9 , 9, std::string) + APC_UPS_VALUED_TEXT_SENSOR(model_name, CTRL_A, CTRL_A, std::string) + APC_UPS_TEXT_SENSOR(last_battery_change_new_date, DATE, DATE) // New battery change date before saving + APC_UPS_VALUED_SWITCH(front_panel_test, A, A, bool) APC_UPS_VALUED_SWITCH(self_test, W, W, bool) APC_UPS_VALUED_SWITCH(start_runtime_calibration, D, D, bool) APC_UPS_VALUED_SWITCH(simulate_power_failure, U, U, bool) + APC_UPS_VALUED_SWITCH(save_last_battery_change_date, SDATE, SDATE, bool) // Save last battery change date + APC_UPS_VALUED_SWITCH(shutdown_with_grace_period, K, K, bool) // Shutdown with grace period + APC_UPS_VALUED_SWITCH(soft_shutdown, S, S, bool) // Soft shutdown + APC_UPS_SWITCH(shutdown_immediately, Z, Z) // Shutdown immediately + APC_UPS_VALUED_SWITCH(turn_on, CTRL_N, CTRL_N, bool) // Turn ON void switch_command(const std::string &command); void setup() override; void loop() override; void dump_config() override; void update() override; + + void set_last_battery_change_new_date(std::string value); // Set New battery change date before saving protected: static const size_t APC_UPS_READ_BUFFER_LENGTH = 110; // maximum supported answer length static const size_t COMMAND_QUEUE_LENGTH = 10; - static const size_t COMMAND_TIMEOUT = 1000; + static const size_t COMMAND_TIMEOUT = 1600; // For send two bytes command (pause between commands for more than 1.5 seconds) uint32_t last_poll_ = 0; void publish_state_(binary_sensor::BinarySensor *binary_sensor, const bool &state); void publish_state_(sensor::Sensor *sensor, float value); @@ -176,6 +189,10 @@ class ApcUps : public uart::UARTDevice, public PollingComponent { uint8_t read_buffer_[APC_UPS_READ_BUFFER_LENGTH]; size_t read_pos_{0}; + uint8_t multibyte_command_ = 0; // State - Sending a multibyte command + const char *command_ctrl_a = "\x01"; + const char *command_ctrl_n = "\x0E"; + uint32_t command_start_millis_ = 0; uint8_t state_; enum State { @@ -189,7 +206,7 @@ class ApcUps : public uart::UARTDevice, public PollingComponent { }; uint8_t last_polling_command_ = 0; - PollingCommand used_polling_commands_[41]; + PollingCommand used_polling_commands_[42]; }; } // namespace apc_ups diff --git a/components/apc_ups/sensor/__init__.py b/components/apc_ups/sensor/__init__.py index cd618e7..955768b 100644 --- a/components/apc_ups/sensor/__init__.py +++ b/components/apc_ups/sensor/__init__.py @@ -3,10 +3,9 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, - CONF_INTERNAL_TEMPERATURE, DEVICE_CLASS_EMPTY, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_TIMELAPSE, @@ -37,7 +36,7 @@ CONF_STATUS_BITMASK = "status_bitmask" CONF_STATE_OF_CHARGE = "state_of_charge" CONF_ESTIMATED_RUNTIME = "estimated_runtime" -# CONF_INTERNAL_TEMPERATURE = "internal_temperature" +CONF_INTERNAL_TEMPERATURE = "internal_temperature" CONF_AMBIENT_HUMIDITY = "ambient_humidity" CONF_AMBIENT_TEMPERATURE = "ambient_temperature" CONF_NOMINAL_BATTERY_VOLTAGE = "nominal_battery_voltage" diff --git a/components/apc_ups/switch/__init__.py b/components/apc_ups/switch/__init__.py index 71fb1cb..3432e88 100644 --- a/components/apc_ups/switch/__init__.py +++ b/components/apc_ups/switch/__init__.py @@ -7,25 +7,26 @@ DEPENDENCIES = ["uart"] -# CONF_BEEPER = "beeper" -CONF_QUICK_TEST = "quick_test" -CONF_DEEP_TEST = "deep_test" -CONF_TEN_MINUTES_TEST = "ten_minutes_test" - CONF_FRONT_PANEL_TEST = "front_panel_test" CONF_SELF_TEST = "self_test" CONF_START_RUNTIME_CALIBRATION = "start_runtime_calibration" CONF_SIMULATE_POWER_FAILURE = "simulate_power_failure" +CONF_SAVE_LAST_BATTERY_CHANGE_DATE = "save_last_battery_change_date" +CONF_SHUTDOWN_WITH_GRACE_PERIOD = "shutdown_with_grace_period" +CONF_SOFT_SHUTDOWN = "soft_shutdown" +CONF_SHUTDOWN_IMMEDIATELY = "shutdown_immediately" +CONF_TURN_ON = "turn_on" TYPES = { - CONF_BEEPER: ("Q", "Q"), - CONF_QUICK_TEST: ("T", "CT"), - CONF_DEEP_TEST: ("TL", "CT"), - CONF_TEN_MINUTES_TEST: ("T10", "CT"), CONF_FRONT_PANEL_TEST: ("A", None), CONF_SELF_TEST: ("W", None), CONF_START_RUNTIME_CALIBRATION: ("D", "D"), CONF_SIMULATE_POWER_FAILURE: ("U", None), + CONF_SAVE_LAST_BATTERY_CHANGE_DATE: ("SDATE", None), + CONF_SHUTDOWN_WITH_GRACE_PERIOD: ("K", None), + CONF_SOFT_SHUTDOWN: ("S", None), + CONF_SHUTDOWN_IMMEDIATELY: ("Z", None), + CONF_TURN_ON: ("CTRL_N", None), } ApcUpsSwitch = apc_ups_ns.class_("ApcUpsSwitch", switch.Switch, cg.Component) diff --git a/components/apc_ups/switch/apc_ups_switch.cpp b/components/apc_ups/switch/apc_ups_switch.cpp index ed952da..f4975ed 100644 --- a/components/apc_ups/switch/apc_ups_switch.cpp +++ b/components/apc_ups/switch/apc_ups_switch.cpp @@ -12,13 +12,14 @@ void ApcUpsSwitch::write_state(bool state) { if (state) { if (this->on_command_.length() > 0) { this->parent_->switch_command(this->on_command_); + this->publish_state(true); } } else { if (this->off_command_.length() > 0) { this->parent_->switch_command(this->off_command_); + this->publish_state(false); } } - this->publish_state(state); } } // namespace apc_ups diff --git a/components/apc_ups/text_sensor/__init__.py b/components/apc_ups/text_sensor/__init__.py index 1fc1fdf..08edfa9 100644 --- a/components/apc_ups/text_sensor/__init__.py +++ b/components/apc_ups/text_sensor/__init__.py @@ -11,6 +11,7 @@ CONF_FIRMWARE_REVISION = "firmware_revision" CONF_MANUFACTURE_DATE = "manufacture_date" CONF_LAST_BATTERY_CHANGE_DATE = "last_battery_change_date" +CONF_LAST_BATTERY_CHANGE_NEW_DATE = "last_battery_change_new_date" CONF_LOCAL_IDENTIFIER = "local_identifier" CONF_SERIAL_NUMBER = "serial_number" CONF_OLD_FIRMWARE_VERSION = "old_firmware_version" @@ -18,7 +19,7 @@ CONF_COPYRIGHT_NOTICE = "copyright_notice" CONF_SELF_TEST_RESULTS = "self_test_results" CONF_ALARM_DELAY = "alarm_delay" -CONF_LINE_QUALITY = "line_quality" +CONF_LINE_QUALITI = "line_quality" CONF_MODEL_NAME = "model_name" TYPES = [ @@ -27,14 +28,15 @@ CONF_FIRMWARE_REVISION, CONF_MANUFACTURE_DATE, CONF_LAST_BATTERY_CHANGE_DATE, + CONF_LAST_BATTERY_CHANGE_NEW_DATE, CONF_LOCAL_IDENTIFIER, CONF_SERIAL_NUMBER, - CONF_OLD_FIRMWARE_VERSION, + CONF_OLD_FIRMWARE_VERSION, CONF_MEASURE_UPC_FIRMWARE, CONF_COPYRIGHT_NOTICE, CONF_SELF_TEST_RESULTS, CONF_ALARM_DELAY, - CONF_LINE_QUALITY, + CONF_LINE_QUALITI, CONF_MODEL_NAME, ] diff --git a/esp-01-example.yaml b/esp-01-example.yaml new file mode 100644 index 0000000..9cfb1dc --- /dev/null +++ b/esp-01-example.yaml @@ -0,0 +1,201 @@ +substitutions: + name: apc-ups + device_description: "Monitor and control a APC UPS via RS232" + external_components_source: github://samoswall/esphome-apc-ups@main + tx_pin: GPIO1 + rx_pin: GPIO3 + +esphome: + name: ${name} + comment: ${device_description} + project: + name: "samoswall.esphome-apc-ups" + version: 1.0.0 + +esp8266: + board: esp01_1m + +external_components: + - source: ${external_components_source} + refresh: 0s + +ota: + password: "12345678" + +api: + encryption: + key: "rbwt+vsrxKOwf6FH53vbMI7TCEvzTiQnn8p2jR9fySY=" + +logger: + level: NONE + +captive_portal: + +web_server: + port: 80 + +mqtt: + broker: !secret mqtt_host + username: !secret mqtt_username + password: !secret mqtt_password + id: mqtt_client + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "Esphome-Apc-Ups Fallback Hotspot" + password: "12345678" + +uart: + - id: uart_0 + baud_rate: 2400 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +apc_ups: + - id: ups0 + uart_id: uart_0 + +binary_sensor: + - platform: apc_ups + apc_ups_id: ups1 + runtime_calibration: + name: "${name} runtime calibration" + smart_trim: + name: "${name} smart trim" + smart_boost: + name: "${name} smart boost" + on_line: + name: "${name} on line" + on_battery: + name: "${name} on battery" + output_overloaded: + name: "${name} output overloaded" + battery_low: + name: "${name} battery low" + replace_battery: + name: "${name} replace battery" + smart_mode: + name: "${name} smart mode" + +switch: + - platform: apc_ups + apc_ups_id: ups1 + front_panel_test: + name: "${name} front panel test" + simulate_power_failure: + name: "${name} simulate power failure" + self_test: + name: "${name} self test" + start_runtime_calibration: + name: "${name} start runtime calibration" + save_last_battery_change_date: + name: "${name} save last battery change date" + shutdown_with_grace_period: + name: "${name} shutdown with grace period" + soft_shutdown: + name: "${name} soft shutdown" + shutdown_immediately: + name: "${name} shutdown immediately" + turn_on: + name: "${name} turn on" + - platform: restart + name: "${devicename} restart" + +sensor: + - platform: apc_ups + apc_ups_id: ups1 + battery_voltage: + name: "${name} battery voltage" + grid_frequency: + name: "${name} grid frequency" + grid_voltage: + name: "${name} grid voltage" + ac_output_voltage: + name: "${name} ac output voltage" + ac_output_load: + name: "${name} ac output load" + status_bitmask: + name: "${name} status bitmask" + state_of_charge: + name: "${name} state of charge" + estimated_runtime: + name: "${name} estimated runtime" + internal_temperature: + name: "${name} internal temperature" +# ambient_temperature: +# name: "${name} ambient temperature" + max_grid_voltage: + name: "${name} max grid voltage" + min_grid_voltage: + name: "${name} min grid voltage" + nominal_battery_voltage: + name: "${name} nominal battery voltage" + nominal_output_voltage: + name: "${name} nominal output voltage" + + - platform: wifi_signal + name: "${devicename} wifi signal" + update_interval: 600s + + - platform: uptime + name: "${devicename} Uptime in Days" + id: uptime_sensor_days + update_interval: 60s + + +text_sensor: + - platform: apc_ups + apc_ups_id: ups1 + cause_of_last_transfer: + name: "${name} cause of last transfer" +# protocol_info: +# name: "${name} protocol info" + firmware_revision: + id: "myfirmware" + name: "${name} firmware revision" + old_firmware_version: + id: myoldfirmware + name: "${name} old firmware version" + manufacture_date: + name: "${name} manufacture date" + last_battery_change_date: + name: "${name} last battery change date" + local_identifier: + name: "${name} local identifier" + serial_number: + name: "${name} serial number" + self_test_results: + name: "${name} self test results" + model_name: + name: "${name} model name" + last_battery_change_new_date: + name: "${name} last battery change new date" + + - platform: version + name: "$devicename Version" + + - platform: wifi_info + ip_address: + name: "$devicename IP" + bssid: + name: "$devicename BSSID" + + +time: + - platform: sntp + id: sntp_time + timezone: Europe/Moscow + + +datetime: + - platform: template + id: my_datetime_date + type: date + name: Change battery date + optimistic: yes + on_value: + then: + - lambda: |- + id(ups1).set_last_battery_change_new_date(x.strftime("%m/%d/%y")); diff --git a/esp32-example.yaml b/esp32-example.yaml index 00b0ab7..b0e4aca 100644 --- a/esp32-example.yaml +++ b/esp32-example.yaml @@ -1,16 +1,15 @@ substitutions: name: apc-ups device_description: "Monitor and control a APC UPS via RS232" - external_components_source: github://syssi/esphome-apc-ups@main + external_components_source: github://samoswall/esphome-apc-ups@main tx_pin: GPIO16 rx_pin: GPIO17 esphome: name: ${name} comment: ${device_description} - min_version: 2024.6.0 project: - name: "syssi.esphome-apc-ups" + name: "samoswall.esphome-apc-ups" version: 1.0.0 esp32: @@ -27,8 +26,6 @@ wifi: password: !secret wifi_password ota: - platform: esphome - logger: # If you don't use Home Assistant please remove this `api` section and uncomment the `mqtt` component! @@ -59,7 +56,7 @@ apc_ups: binary_sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 runtime_calibration: name: "${name} runtime calibration" smart_trim: @@ -81,7 +78,7 @@ binary_sensor: switch: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 front_panel_test: name: "${name} front panel test" simulate_power_failure: @@ -90,10 +87,22 @@ switch: name: "${name} self test" start_runtime_calibration: name: "${name} start runtime calibration" + save_last_battery_change_date: + name: "${name} save last battery change date" + shutdown_with_grace_period: + name: "${name} shutdown with grace period" + soft_shutdown: + name: "${name} soft shutdown" + shutdown_immediately: + name: "${name} shutdown immediately" + turn_on: + name: "${name} turn on" + - platform: restart + name: "${devicename} restart" sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 battery_voltage: name: "${name} battery voltage" grid_frequency: @@ -112,8 +121,8 @@ sensor: name: "${name} estimated runtime" internal_temperature: name: "${name} internal temperature" - ambient_temperature: - name: "${name} ambient temperature" +# ambient_temperature: +# name: "${name} ambient temperature" max_grid_voltage: name: "${name} max grid voltage" min_grid_voltage: @@ -123,19 +132,28 @@ sensor: nominal_output_voltage: name: "${name} nominal output voltage" + - platform: wifi_signal + name: "${devicename} wifi signal" + update_interval: 600s + + - platform: uptime + name: "${devicename} Uptime in Days" + id: uptime_sensor_days + update_interval: 60s + + text_sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 cause_of_last_transfer: name: "${name} cause of last transfer" - # Could cause a connection reset of the Home Assistant API connection - # See https://github.com/syssi/esphome-apc-ups/issues/1 - # - # protocol_info: - # name: "${name} protocol info" +# protocol_info: +# name: "${name} protocol info" firmware_revision: + id: "myfirmware" name: "${name} firmware revision" old_firmware_version: + id: myoldfirmware name: "${name} old firmware version" manufacture_date: name: "${name} manufacture date" @@ -149,3 +167,32 @@ text_sensor: name: "${name} self test results" model_name: name: "${name} model name" + last_battery_change_new_date: + name: "${name} last battery change new date" + + - platform: version + name: "$devicename Version" + + - platform: wifi_info + ip_address: + name: "$devicename IP" + bssid: + name: "$devicename BSSID" + + +time: + - platform: sntp + id: sntp_time + timezone: Europe/Moscow + + +datetime: + - platform: template + id: my_datetime_date + type: date + name: Change battery date + optimistic: yes + on_value: + then: + - lambda: |- + id(ups1).set_last_battery_change_new_date(x.strftime("%m/%d/%y")); diff --git a/esp8266-example.yaml b/esp8266-example.yaml index 5555dc4..d05ee07 100644 --- a/esp8266-example.yaml +++ b/esp8266-example.yaml @@ -1,16 +1,15 @@ substitutions: name: apc-ups device_description: "Monitor and control a APC UPS via RS232" - external_components_source: github://syssi/esphome-apc-ups@main + external_components_source: github://samoswall/esphome-apc-ups@main tx_pin: GPIO4 rx_pin: GPIO5 esphome: name: ${name} comment: ${device_description} - min_version: 2024.6.0 project: - name: "syssi.esphome-apc-ups" + name: "samoswall.esphome-apc-ups" version: 1.0.0 esp8266: @@ -25,8 +24,6 @@ wifi: password: !secret wifi_password ota: - platform: esphome - logger: # If you don't use Home Assistant please remove this `api` section and uncomment the `mqtt` component! @@ -57,7 +54,7 @@ apc_ups: binary_sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 runtime_calibration: name: "${name} runtime calibration" smart_trim: @@ -79,7 +76,7 @@ binary_sensor: switch: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 front_panel_test: name: "${name} front panel test" simulate_power_failure: @@ -88,10 +85,22 @@ switch: name: "${name} self test" start_runtime_calibration: name: "${name} start runtime calibration" + save_last_battery_change_date: + name: "${name} save last battery change date" + shutdown_with_grace_period: + name: "${name} shutdown with grace period" + soft_shutdown: + name: "${name} soft shutdown" + shutdown_immediately: + name: "${name} shutdown immediately" + turn_on: + name: "${name} turn on" + - platform: restart + name: "${devicename} restart" sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 battery_voltage: name: "${name} battery voltage" grid_frequency: @@ -110,8 +119,8 @@ sensor: name: "${name} estimated runtime" internal_temperature: name: "${name} internal temperature" - ambient_temperature: - name: "${name} ambient temperature" +# ambient_temperature: +# name: "${name} ambient temperature" max_grid_voltage: name: "${name} max grid voltage" min_grid_voltage: @@ -121,19 +130,28 @@ sensor: nominal_output_voltage: name: "${name} nominal output voltage" + - platform: wifi_signal + name: "${devicename} wifi signal" + update_interval: 600s + + - platform: uptime + name: "${devicename} Uptime in Days" + id: uptime_sensor_days + update_interval: 60s + + text_sensor: - platform: apc_ups - apc_ups_id: ups0 + apc_ups_id: ups1 cause_of_last_transfer: name: "${name} cause of last transfer" - # Could cause a connection reset of the Home Assistant API connection - # See https://github.com/syssi/esphome-apc-ups/issues/1 - # - # protocol_info: - # name: "${name} protocol info" +# protocol_info: +# name: "${name} protocol info" firmware_revision: + id: "myfirmware" name: "${name} firmware revision" old_firmware_version: + id: myoldfirmware name: "${name} old firmware version" manufacture_date: name: "${name} manufacture date" @@ -147,3 +165,32 @@ text_sensor: name: "${name} self test results" model_name: name: "${name} model name" + last_battery_change_new_date: + name: "${name} last battery change new date" + + - platform: version + name: "$devicename Version" + + - platform: wifi_info + ip_address: + name: "$devicename IP" + bssid: + name: "$devicename BSSID" + + +time: + - platform: sntp + id: sntp_time + timezone: Europe/Moscow + + +datetime: + - platform: template + id: my_datetime_date + type: date + name: Change battery date + optimistic: yes + on_value: + then: + - lambda: |- + id(ups1).set_last_battery_change_new_date(x.strftime("%m/%d/%y"));