I have a Growatt SPH6000 hybrid inverter connected to 2 strings of solar panels and a Growatt GBLI6532 battery. Originally I used the supplied Growatt Shine-S stick (diverting via grott) to get the lovely data out of it, but I didn’t like having an external dependency, so I decided to try and replace it with an ESP32 running ESPHome.
Here Be Dragons
This is my setup that works for me. There is no guarantee that if you try this, it won’t burn your house down. It may make you fall in love with a penguin and give you nightmares about circus midgets.
Hardware
In a burst of caution, I’m not going to give detailed wiring instructions as a skill filter. If you don’t know how to connect this up, you really shouldn’t be messing with this stuff. The fundamentals are an ESP32 powered via a voltage converter that is connected to a pin on the RS232 socket that appears to give out circa 9-12v. An RS232 level shifter then feeds into the serial port of the ESP32. I spent a fair time with a Raspberry Pi, 2 USB-RS232 cables and a hacked together cable to sniff the traffic between the Growatt and the Shine-S stick to figure out how Growatt told the inverter to do things. These two PDFs helped decode what went on.
Software
substitutions:
devicename: SPH
update_fast: 10s
update_slow: 30s
script:
- id: setClock
then:
- lambda: |-
esphome::modbus_controller::ModbusController *controller = id(growatt);
time_t now = ::time(nullptr);
struct tm *time_info = ::localtime(&now);
int seconds = time_info->tm_sec;
int minutes = time_info->tm_min;
int hour = time_info->tm_hour;
int day = time_info->tm_mday;
int month = time_info->tm_mon + 1;
int year = time_info->tm_year;
year = year + 1900;
// if there is no internet connection localtime returns year 70
if (year != 1970) {
// create the payload
std::vector<uint16_t> rtc_data = {uint16_t(year-2000), uint16_t(month), uint16_t(day), uint16_t(hour),uint16_t(minutes),uint16_t(seconds)};
// Create a modbus command item with the time information as the payload
esphome::modbus_controller::ModbusCommandItem set_rtc_command =
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller, 45, rtc_data.size(), rtc_data);
// Submit the command to the send queue
controller->queue_command(set_rtc_command);
ESP_LOGI("ModbusLambda", "Growatt RTC set to %02d:%02d:%02d %02d/%02d/%04d", hour, minutes, seconds, day, month, year);
}
- id: setAllTimes
then:
- lambda: |-
esphome::modbus_controller::ModbusController *controller = id(growatt);
std::vector<uint16_t> disable_data = {0,0,0};
std::vector<uint16_t> enable_data = {0,23*256+59,1};
std::vector<uint16_t> window_data = {0,23*256+59,0};
std::vector<uint16_t> morning_data = {2*256+0,5*256+0,1};
std::vector<uint16_t> charge_rates = {100,100,1};
int size = window_data.size();
ESP_LOGI("ModbusLambda","Enqueue Writes");
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1090,charge_rates.size(),charge_rates));
//GF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1080,size,disable_data));
//GF2
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1083,size,disable_data));
//GF3
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1086,size,disable_data));
//BF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1100,size,morning_data));
//BF2
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1103,size,disable_data));
//BF3
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1106,size,disable_data));
//LF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1110,size,disable_data));
//LF2
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1113,size,disable_data));
//LF3
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1116,size,disable_data));
ESP_LOGI("ModbusLambda","Writes");
esphome:
name: growatt
friendly_name: Growatt
on_boot:
priority: -100
then:
- delay: 15s
- logger.log: "************* In da boot *******************"
- script.execute:
id: setClock
- script.execute:
id: setAllTimes
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
baud_rate: 0
logs:
json: ERROR
sensor: ERROR
modbus_controller.sensor: ERROR
component: ERROR
# Enable Home Assistant API
api:
encryption:
key: "<SECRET>"
ota:
platform: esphome
password: "<SECRET>"
wifi:
ssid: !secret ios_wifi_ssid
password: !secret ios_wifi_password
domain: .wifi
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Growatt Fallback Hotspot"
password: "<SECRET>"
captive_portal:
web_server:
local: True
port: 80
version: 3
# include_internal: true
mqtt:
broker: alfred.lan
username: !secret ios_mqtt_user
password: !secret ios_mqtt_password
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 10
then:
- script.execute:
id: setClock
uart:
id: mod_bus
tx_pin: 1
rx_pin: 3
baud_rate: 9600
# debug:
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
## the Modbus device addr
address: 0x1
modbus_id: modbus1
setup_priority: -10
update_interval: ${update_slow}
button:
- platform: template
name: "Battery First"
on_press:
then:
lambda: |-
esphome::modbus_controller::ModbusController *controller = id(growatt);
std::vector<uint16_t> on={0,23*256+59,1};
std::vector<uint16_t> off={0,23*256+59,0};
int size = on.size();
ESP_LOGI("ModbusLambda","Enqueue Writes");
//BF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1100,size,on));
//LF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1110,size,off));
//GF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1080,size,off));
ESP_LOGI("ModbusLambda","Writes");
- platform: template
name: "Load First"
on_press:
then:
lambda: |-
esphome::modbus_controller::ModbusController *controller = id(growatt);
std::vector<uint16_t> on={0,23*256+59,1};
std::vector<uint16_t> off={0,23*256+59,0};
int size = on.size();
ESP_LOGI("ModbusLambda","Enqueue Writes");
//BF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1100,size,off));
//LF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1110,size,on));
//GF1
controller->queue_command(esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller,1080,size,off));
ESP_LOGI("ModbusLambda","Writes");
text_sensor:
# - name: "dump"
# platform: modbus_controller
# address: 1080
# register_count: 40
# raw_encode: HEXBYTES
# register_type: "holding"
- platform: template
name: "${devicename} Status"
icon: mdi:eye
entity_category: diagnostic
lambda: |-
if ((id(inv_status).state) == 1) {
return {"Normal"};
} else if ((id(inv_status).state) == 0) {
return {"Standby"};
} else if ((id(inv_status).state) == 2) {
return {"Discharge"};
} else if ((id(inv_status).state) == 3) {
return {"Fault"};
} else if ((id(inv_status).state) == 4) {
return {"Flash"};
} else if ((id(inv_status).state) == 5) {
return {"PV Charging"};
} else if ((id(inv_status).state) == 6) {
return {"AC Charging"};
} else if ((id(inv_status).state) == 7) {
return {"Combined Charging"};
} else if ((id(inv_status).state) == 8) {
return {"Combined Charging & Bypass"};
} else if ((id(inv_status).state) == 9) {
return {"PV Charging & Bypass"};
} else if ((id(inv_status).state) == 10) {
return {"AC Charging & Bypass"};
} else if ((id(inv_status).state) == 11) {
return {"Bypass"};
} else if (id(inv_status).state == 12) {
return {"PV Charge and Discharge"};
} else {
return {"Unknown"};
}
select:
- platform: modbus_controller
name: "${devicename} AC Charging"
icon: mdi:battery-charging-100
address: 1092
value_type: U_WORD
optionsmap:
"Disabled": 0
"Enabled": 1
- platform: modbus_controller
name: "${devicename} Inverter Priority"
icon: mdi:arrow-decision-outline
address: 1044
value_type: U_WORD
optionsmap:
"Load First": 0
"Battery First": 1
"Grid First": 2
sensor:
- platform: modbus_controller
name: "${devicename} Inverter Status"
address: 0
register_type: "read"
icon: mdi:home-lightning-bolt
value_type: U_WORD
id: inv_status
internal: true
- platform: modbus_controller
name: "${devicename} Input Power"
address: 1
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-power-variant
value_type: U_WORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} PV1 Power"
id: pv1_power
address: 5
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 0
internal: true
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} PV2 Power"
id: pv2_power
address: 9
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 0
internal: true
filters:
- multiply: 0.1
- platform: template
id: pv_power
name: "${devicename} PV Power"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-power-variant
accuracy_decimals: 0
lambda: |-
return float((id(pv1_power).state + id(pv2_power).state));
update_interval: ${update_fast}
- platform: template
name: "${devicename} Output Power to grid"
id: ogrid_power
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-power-variant
accuracy_decimals: 0
lambda: |-
return max(0.0f,float((id(out_power).state - id(charge_power).state - id(invlocal_power).state)));
update_interval: ${update_fast}
- platform: modbus_controller
name: "${devicename} Output Power"
id: out_power
address: 35
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:home-lightning-bolt
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
id: pv1_energy_today
name: "${devicename} PV1 Energy Today"
address: 59
register_type: "read"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 1
internal: true
filters:
- multiply: 0.1
- platform: modbus_controller
id: pv1_energy_total
name: "${devicename} PV1 Energy Total"
address: 61
register_type: "read"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 1
internal: true
filters:
- multiply: 0.1
- platform: modbus_controller
id: pv2_energy_today
name: "${devicename} PV2 Energy Today"
address: 63
register_type: "read"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 1
internal: true
filters:
- multiply: 0.1
- platform: modbus_controller
id: pv2_energy_total
name: "${devicename} PV2 Energy Total"
address: 65
register_type: "read"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
value_type: U_DWORD
accuracy_decimals: 1
internal: true
filters:
- multiply: 0.1
- platform: template
id: pv_energy_today
name: "${devicename} PV Energy Today"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
accuracy_decimals: 1
lambda: |-
return float((id(pv1_energy_today).state + id(pv2_energy_today).state));
update_interval: ${update_slow}
- platform: template
id: pv_energy_total
name: "${devicename} PV Energy Total"
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power-variant
accuracy_decimals: 1
lambda: |-
return float((id(pv1_energy_total).state + id(pv2_energy_total).state));
update_interval: ${update_slow}
- platform: modbus_controller
name: "${devicename} Inverter Temperature"
address: 93
register_type: "read"
unit_of_measurement: °C
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Discharging Power"
address: 1009
register_type: "read"
unit_of_measurement: W
device_class: power
icon: mdi:battery-arrow-down
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Charging Power"
address: 1011
id: charge_power
register_type: "read"
unit_of_measurement: W
device_class: power
icon: mdi:battery-arrow-up-outline
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery SoC"
address: 1014
register_type: "read"
unit_of_measurement: "%"
icon: mdi:home-battery
value_type: U_WORD
accuracy_decimals: 0
- platform: modbus_controller
name: "${devicename} AC Power to User Total"
address: 1021
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} AC Power to Grid Total"
address: 1029
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} INV Power to Local Load Total"
id: invlocal_power
address: 1037
register_type: "read"
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:home-import-outline
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Temperature"
address: 1040
register_type: "read"
unit_of_measurement: °C
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
# filters:
# - multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery State"
address: 1041
register_type: "read"
icon: mdi:home-battery
value_type: U_WORD
- platform: modbus_controller
name: "${devicename} Energy to User Today"
address: 1044
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Energy to User Total"
address: 1046
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Energy to Grid Today"
address: 1048
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Energy to Grid Total"
address: 1050
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Discharge Today"
address: 1052
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:battery-arrow-down
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Discharge Total"
address: 1054
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:battery-arrow-down
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Charge Today"
address: 1056
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:battery-arrow-up-outline
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Battery Charge Total"
address: 1058
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:battery-arrow-up-outline
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Local Load Today"
address: 1060
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:home-import-outline
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "${devicename} Local Load Total"
address: 1062
register_type: "read"
unit_of_measurement: kWh
state_class: total_increasing
device_class: energy
icon: mdi:home-import-outline
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
number:
- platform: modbus_controller
name: "${devicename} Battery First Charge Rate"
min_value: 0
max_value: 100
address: 1090
register_type: "holding"
- platform: modbus_controller
name: "${devicename} Battery First Stop SoC"
min_value: 0
max_value: 100
address: 1091
register_type: "holding"
Don’t be put off by the length, most of it is just definitions of fields. The definitions I figured out from other places on the internet and this document. The tricky bit was getting all the writes to work.
The inverter appears to be somewhat transactional. There are a number of fields that have to be written as a group to work, typically any field relating to times. Using the drop down to set the Load/Grid/Battery first doesn’t work, you have to set it via the lambdas attached to the buttons. I didn’t write a ‘Grid First’ button as I never do that.
setAllTimes
sets defaults behaviour for the Octopus tariff I used to be on, but Home Assistant controls that anyway so I hadn’t noticed it was out of date until now :)
I’ve been running this now for well over a year with few problems, it just quietly does its thing. All automations around when to actually charge are controlled by Home Assistant. I spit the data out to MQTT as well so that my OpenEVSE car charger can listen in and turn on if it is particularly sunny but otherwise I tend to just forget about this and it quietly chugs away in the loft.
I hope this helps as some sort of reference.