diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d6f5548..67182ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -169,6 +169,7 @@ jobs: esphome -s external_components_source components config esp8266-solar-charger-example-faker.yaml esphome -s external_components_source components config esp8266-charger-example-faker.yaml esphome -s external_components_source components config esp8266-charging-converter-example-faker.yaml + esphome -s external_components_source components config esp8266-smart-shunt-example-faker.yaml - run: | esphome -s external_components_source ../components config tests/esp8266-fake-charger.yaml esphome -s external_components_source ../components config tests/esp8266-fake-charging-converter.yaml @@ -215,3 +216,4 @@ jobs: esphome -s external_components_source components compile esp8266-solar-charger-example-faker.yaml esphome -s external_components_source components compile esp8266-charger-example-faker.yaml esphome -s external_components_source components compile esp8266-charging-converter-example-faker.yaml + esphome -s external_components_source components compile esp8266-smart-shunt-example-faker.yaml diff --git a/components/votronic/votronic.cpp b/components/votronic/votronic.cpp index 63abd9f..d10bbd1 100644 --- a/components/votronic/votronic.cpp +++ b/components/votronic/votronic.cpp @@ -6,6 +6,9 @@ namespace esphome { namespace votronic { static const char *const TAG = "votronic"; +static const char *const TAG_INFO1 = "votronic.i1"; +static const char *const TAG_INFO2 = "votronic.i2"; +static const char *const TAG_INFO3 = "votronic.i3"; static const uint8_t VOTRONIC_FRAME_START = 0xAA; static const uint8_t VOTRONIC_FRAME_LENGTH = 16; @@ -16,7 +19,7 @@ static const uint8_t VOTRONIC_FRAME_TYPE_CHARGING_CONVERTER = 0x7A; static const uint8_t VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO1 = 0xCA; static const uint8_t VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO2 = 0xDA; -static const uint8_t VOTRONIC_FRAME_TYPE_UNNAMED = 0xFA; +static const uint8_t VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO3 = 0xFA; static const uint8_t VOTRONIC_FRAME_TYPE_CONTROL_CHARGER = 0x7A; // Incorrect protocol description? static const uint8_t VOTRONIC_FRAME_TYPE_CONTROL_CHARGING_CONVERTER = 0x3A; // Incorrect protocol description? @@ -132,8 +135,14 @@ void Votronic::on_votronic_data(const std::vector &data) { this->decode_charger_data_(frame_type, data); break; case VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO1: + this->decode_battery_computer_info1_data_(data); + break; case VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO2: - case VOTRONIC_FRAME_TYPE_UNNAMED: + this->decode_battery_computer_info2_data_(data); + break; + case VOTRONIC_FRAME_TYPE_BATTERY_COMPUTER_INFO3: + this->decode_battery_computer_info3_data_(data); + break; default: ESP_LOGW(TAG, "Your device is probably not supported. Please create an issue here: " "https://github.com/syssi/esphome-votronic/issues"); @@ -153,6 +162,9 @@ void Votronic::decode_solar_charger_data_(const std::vector &data) { return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); }; + ESP_LOGI(TAG, "Solar charger data received"); + ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str()); + // Byte Len Payload Description Unit Precision // 0 1 0xAA Sync Byte // 1 1 0x1A Frame Type @@ -196,6 +208,9 @@ void Votronic::decode_charger_data_(const uint8_t &frame_type, const std::vector return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); }; + ESP_LOGI(TAG, "Charger data received"); + ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str()); + // Byte Len Payload Description Unit Precision // 0 1 0xAA Sync Byte // 1 1 0x3A Frame Type @@ -214,7 +229,7 @@ void Votronic::decode_charger_data_(const uint8_t &frame_type, const std::vector // 9 1 0x00 Reserved // 10 1 0xA0 Charging Power % 0-100% 1%/Bit this->publish_state_(this->state_of_charge_sensor_, (float) data[10]); - // 11 1 0x15 Reserved + // 11 1 0x15 Controller temperature this->publish_state_(this->controller_temperature_sensor_, (float) data[11]); // 12 1 0x03 Charging mode setting (dip switches) this->publish_state_(this->charging_mode_setting_id_sensor_, data[12]); @@ -231,6 +246,136 @@ void Votronic::decode_charger_data_(const uint8_t &frame_type, const std::vector this->publish_state_(this->aes_active_binary_sensor_, (data[14] & (1 << 5))); } +void Votronic::decode_battery_computer_info1_data_(const std::vector &data) { + const uint32_t now = millis(); + if (now - this->last_battery_computer_info1_data_ < this->throttle_) { + return; + } + this->last_battery_computer_info1_data_ = now; + + auto votronic_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); + }; + + // Example frame of a Votronic Smart Shunt 400 S: + // 0xAA 0xCA 0x03 0x05 0x0F 0x05 0xC7 0x01 0x20 0x00 0x63 0x00 0x7B 0xFE 0xFF 0x39 + + ESP_LOGI(TAG, "Battery computer info1 data received"); + ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str()); + + // Byte Len Payload Description Unit Precision + // 0 1 0xAA Sync Byte + // 1 1 0xCA Frame Type + // 2 2 0x03 0x05 Battery Voltage + float battery_voltage = votronic_get_16bit(2) * 0.01f; + this->publish_state_(this->battery_voltage_sensor_, battery_voltage); + // 4 2 0x0F 0x05 Second Battery Voltage + this->publish_state_(this->secondary_battery_voltage_sensor_, votronic_get_16bit(4) * 0.01f); + // 6 2 0xC7 0x01 + ESP_LOGI(TAG_INFO1, "Capacity remaining: %.0f Ah", votronic_get_16bit(6) * 1.0f); + // 8 2 0x20 0x00 + ESP_LOGD(TAG_INFO1, "Byte 8-9: 0x%02X 0x%02X / %d %d / %d", data[8], data[9], data[8], data[9], + votronic_get_16bit(8)); + // 10 2 0x63 0x00 + this->publish_state_(this->state_of_charge_sensor_, (float) data[10]); + // 12 2 0x7B 0xFE + float current = ((int16_t) votronic_get_16bit(12)) * 0.001f; + this->publish_state_(this->current_sensor_, current); + this->publish_state_(this->power_sensor_, current * battery_voltage); + this->publish_state_(this->charging_binary_sensor_, (current > 0.0f)); + this->publish_state_(this->discharging_binary_sensor_, (current < 0.0f)); + + // 14 1 0xFF + ESP_LOGI(TAG_INFO1, "Byte 14: 0x%02X / %d", data[14], data[14]); + // 15 1 0x39 CRC +} + +void Votronic::decode_battery_computer_info2_data_(const std::vector &data) { + const uint32_t now = millis(); + if (now - this->last_battery_computer_info2_data_ < this->throttle_) { + return; + } + this->last_battery_computer_info2_data_ = now; + + auto votronic_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); + }; + + // Example frame of a Votronic Smart Shunt 400 S: + // 0xAA 0xDA 0x00 0x00 0x00 0x00 0xF8 0x11 0x5E 0x07 0x00 0x00 0x2F 0x04 0x02 0x43 + + ESP_LOGI(TAG, "Battery computer info2 data received"); + ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str()); + + // Byte Len Payload Description Unit Precision + // 0 1 0xAA Sync Byte + // 1 1 0xDA Frame Type + // 2 2 0x00 0x00 + ESP_LOGD(TAG_INFO2, "Byte 2-3: 0x%02X 0x%02X / %d %d / %d", data[2], data[3], data[2], data[3], + votronic_get_16bit(2)); + // 4 2 0x00 0x00 + ESP_LOGD(TAG_INFO2, "Byte 4-5: 0x%02X 0x%02X / %d %d / %d", data[4], data[5], data[4], data[5], + votronic_get_16bit(4)); + // 6 2 0xF8 0x11 + ESP_LOGI(TAG_INFO2, "Nominal capacity: %.1f Ah", votronic_get_16bit(6) * 0.1f); + // 8 2 0x5E 0x07 + ESP_LOGD(TAG_INFO2, "Byte 8-9: 0x%02X 0x%02X / %d %d / %d", data[8], data[9], data[8], data[9], + votronic_get_16bit(8)); + // 10 2 0x00 0x00 + ESP_LOGD(TAG_INFO2, "Byte 10-11: 0x%02X 0x%02X / %d %d / %d", data[10], data[11], data[10], data[11], + votronic_get_16bit(10)); + // 12 1 0x2F + ESP_LOGI(TAG_INFO2, "Battery setting: 0x%02X / %d", data[12], data[12]); + // 13 1 0x04 + ESP_LOGD(TAG_INFO2, "Byte 13: 0x%02X / %d", data[13], data[13]); + // 14 1 0x02 + ESP_LOGD(TAG_INFO2, "Byte 14: 0x%02X / %d", data[14], data[14]); + // 15 1 0x43 CRC +} + +void Votronic::decode_battery_computer_info3_data_(const std::vector &data) { + const uint32_t now = millis(); + if (now - this->last_battery_computer_info3_data_ < this->throttle_) { + return; + } + this->last_battery_computer_info3_data_ = now; + + auto votronic_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); + }; + + // Example frame of a Votronic Smart Shunt 400 S: + // 0xAA 0xFA 0x2F 0x00 0x00 0x00 0xD2 0x02 0x00 0x0A 0x00 0x00 0x28 0xD0 0x00 0xF7 + + ESP_LOGI(TAG, "Battery computer info3 data received"); + ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str()); + + // Byte Len Payload Description Unit Precision + // 0 1 0xAA Sync Byte + // 1 1 0xFA Frame Type + // 2 2 0x2F 0x00 + ESP_LOGD(TAG_INFO3, "Byte 2-3: 0x%02X 0x%02X / %d %d / %d", data[2], data[3], data[2], data[3], + votronic_get_16bit(2)); + // 4 2 0x00 0x00 + ESP_LOGD(TAG_INFO3, "Byte 4-5: 0x%02X 0x%02X / %d %d / %d", data[4], data[5], data[4], data[5], + votronic_get_16bit(4)); + // 6 2 0xD2 0x02 + ESP_LOGD(TAG_INFO3, "Byte 6-7: 0x%02X 0x%02X / %d %d / %d", data[6], data[7], data[6], data[7], + votronic_get_16bit(6)); + // 8 2 0x00 0x0A + ESP_LOGD(TAG_INFO3, "Byte 8-9: 0x%02X 0x%02X / %d %d / %d", data[8], data[9], data[8], data[9], + votronic_get_16bit(8)); + // 10 2 0x00 0x00 + ESP_LOGD(TAG_INFO3, "Byte 10-11: 0x%02X 0x%02X / %d %d / %d", data[10], data[11], data[10], data[11], + votronic_get_16bit(10)); + // 12 2 0x28 0xD0 + ESP_LOGD(TAG_INFO3, "Byte 12-13: 0x%02X 0x%02X / %d %d / %d", data[12], data[13], data[12], data[13], + votronic_get_16bit(12)); + // 14 1 0x00 + ESP_LOGD(TAG_INFO3, "Byte 14: 0x%02X / %d", data[14], data[14]); + // 15 1 0xF7 CRC +} + void Votronic::dump_config() { ESP_LOGCONFIG(TAG, "Votronic:"); ESP_LOGCONFIG(TAG, " RX timeout: %d ms", this->rx_timeout_); diff --git a/components/votronic/votronic.h b/components/votronic/votronic.h index af44aca..562b02b 100644 --- a/components/votronic/votronic.h +++ b/components/votronic/votronic.h @@ -109,11 +109,17 @@ class Votronic : public uart::UARTDevice, public PollingComponent { uint32_t last_byte_{0}; uint32_t last_solar_charger_data_{0}; uint32_t last_charger_data_{0}; + uint32_t last_battery_computer_info1_data_{0}; + uint32_t last_battery_computer_info2_data_{0}; + uint32_t last_battery_computer_info3_data_{0}; uint16_t throttle_; uint16_t rx_timeout_{150}; void decode_solar_charger_data_(const std::vector &data); void decode_charger_data_(const uint8_t &frame_type, const std::vector &data); + void decode_battery_computer_info1_data_(const std::vector &data); + void decode_battery_computer_info2_data_(const std::vector &data); + void decode_battery_computer_info3_data_(const std::vector &data); bool parse_votronic_byte_(uint8_t byte); void publish_state_(binary_sensor::BinarySensor *binary_sensor, const bool &state); void publish_state_(sensor::Sensor *sensor, float value); diff --git a/esp8266-smart-shunt-example-debug.yaml b/esp8266-smart-shunt-example-debug.yaml new file mode 100644 index 0000000..447eb49 --- /dev/null +++ b/esp8266-smart-shunt-example-debug.yaml @@ -0,0 +1,27 @@ +<<: !include esp8266-smart-shunt-example.yaml + +logger: + level: VERY_VERBOSE + logs: + votronic: VERY_VERBOSE + component: DEBUG + scheduler: INFO + binary_sensor: DEBUG + sensor: DEBUG + text_sensor: DEBUG + mqtt: INFO + mqtt.idf: INFO + mqtt.component: INFO + mqtt.sensor: INFO + mqtt.switch: INFO + api.service: INFO + api: INFO + +uart: + - id: uart0 + baud_rate: 1000 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + debug: + direction: BOTH + dummy_receiver: false diff --git a/esp8266-smart-shunt-example-faker.yaml b/esp8266-smart-shunt-example-faker.yaml new file mode 100644 index 0000000..bda8f34 --- /dev/null +++ b/esp8266-smart-shunt-example-faker.yaml @@ -0,0 +1,19 @@ +<<: !include esp8266-smart-shunt-example-debug.yaml + +interval: + - interval: 4s + then: + - lambda: |- + id(votronic0).on_votronic_data({ + 0xAA, 0xCA, 0x03, 0x05, 0x0F, 0x05, 0xC7, 0x01, 0x20, 0x00, 0x63, 0x00, 0x7B, 0xFE, 0xFF, 0x39 + }); + - delay: 1s + - lambda: |- + id(votronic0).on_votronic_data({ + 0xAA, 0xDA, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x11, 0x5E, 0x05, 0x00, 0x00, 0x2F, 0x04, 0x02, 0x41 + }); + - delay: 1s + - lambda: |- + id(votronic0).on_votronic_data({ + 0xAA, 0xFA, 0x2F, 0x00, 0x00, 0x00, 0xD2, 0x02, 0x00, 0x0A, 0x00, 0x00, 0x28, 0xD0, 0x00, 0xF7 + }); diff --git a/esp8266-smart-shunt-example.yaml b/esp8266-smart-shunt-example.yaml new file mode 100644 index 0000000..11e616c --- /dev/null +++ b/esp8266-smart-shunt-example.yaml @@ -0,0 +1,71 @@ +substitutions: + name: votronic-solar-charger + device_description: "Monitor a Votronic Solar Charger via the display link port (UART)" + external_components_source: github://syssi/esphome-votronic@main + tx_pin: GPIO4 + rx_pin: GPIO5 + rx_timeout: 150ms + +esphome: + name: ${name} + comment: ${device_description} + project: + name: "syssi.esphome-votronic" + version: 1.1.0 + +esp8266: + board: d1_mini + +external_components: + - source: ${external_components_source} + refresh: 0s + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +ota: +logger: + +# If you use Home Assistant please remove this `mqtt` section and uncomment the native `api` component! +# api: + +mqtt: + broker: !secret mqtt_host + username: !secret mqtt_username + password: !secret mqtt_password + id: mqtt_client + +uart: + - id: uart0 + baud_rate: 1000 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +votronic: + - id: votronic0 + uart_id: uart0 + rx_timeout: ${rx_timeout} + throttle: 2s + +binary_sensor: + - platform: votronic + votronic_id: votronic0 + charging: + name: "${name} charging" + discharging: + name: "${name} discharging" + +sensor: + - platform: votronic + votronic_id: votronic0 + battery_voltage: + name: "${name} battery voltage" + secondary_battery_voltage: + name: "${name} secondary battery voltage" + current: + name: "${name} current" + power: + name: "${name} power" + state_of_charge: + name: "${name} state of charge"