mirror of
https://github.com/syssi/esphome-votronic.git
synced 2025-07-22 12:10:34 +02:00
Add basic smart shunt support (#29)
This commit is contained in:
committed by
GitHub
parent
6c3c0ced7c
commit
e85083e85e
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
@@ -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_);
|
||||
|
@@ -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);
|
||||
|
27
esp8266-smart-shunt-example-debug.yaml
Normal file
27
esp8266-smart-shunt-example-debug.yaml
Normal 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
|
19
esp8266-smart-shunt-example-faker.yaml
Normal file
19
esp8266-smart-shunt-example-faker.yaml
Normal 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
|
||||
});
|
71
esp8266-smart-shunt-example.yaml
Normal file
71
esp8266-smart-shunt-example.yaml
Normal 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"
|
Reference in New Issue
Block a user