Add basic smart shunt support (#29)

This commit is contained in:
Sebastian Muszynski
2023-04-14 10:16:52 +02:00
committed by GitHub
parent 6c3c0ced7c
commit e85083e85e
6 changed files with 273 additions and 3 deletions

View File

@@ -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

View File

@@ -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<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint8_t> &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_);

View File

@@ -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<uint8_t> &data);
void decode_charger_data_(const uint8_t &frame_type, const std::vector<uint8_t> &data);
void decode_battery_computer_info1_data_(const std::vector<uint8_t> &data);
void decode_battery_computer_info2_data_(const std::vector<uint8_t> &data);
void decode_battery_computer_info3_data_(const std::vector<uint8_t> &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);

View File

@@ -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

View File

@@ -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
});

View File

@@ -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"