Merge pull request #1367 from dalathegreat/feature/growatt-WIT

New Inverter  Add initial support for Growatt Wit CAN
This commit is contained in:
Daniel Öster 2025-08-03 21:00:14 +03:00 committed by GitHub
commit 27f95c5388
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 289 additions and 0 deletions

View file

@ -0,0 +1,154 @@
#include "GROWATT-WIT-CAN.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
/* TODO:
This protocol has not been tested with any inverter. Proceed with extreme caution.
Search the file for "TODO" to see all the places that might require work
Largest TODO: Map all CAN message .ID properly
Example, how should 1AC6XXXX be transmitted? .ID = 0x1AC6, at the time being, but we need to add the targegt address and source address.
GROWATT BATTERY BMS CAN COMMUNICATION PROTOCOL V1.1 2024.7.19
29-bit identifier
500kBit/sec
Big-endian
Internal CAN: The extended frame CAN_ID is 29 bits, and the 29-bit CAN_ID is divided into five parts:
PRI, PG, DA, SA, and FSN.
- PRI: indicates priority.
- PG: indicates the page number. If the FSN is not enough, you can turn the page to increase the range of the FSN.
- TA: indicates the target address.
- SA: indicates the source address.
- FSN: represents functional domain.
(See the Wiki for full documentation explained)
Example: 0x1AC0FFF3: PRI is 6, PG is 2, FSN is 0xC0, TA is 0xFF, SA is 0xF3
*/
void GrowattWitInverter::update_values() {
//Maximum allowable charging current of the battery system, 0-10000 dA
GROWATT_1AC3XXXX.data.u8[0] = (datalayer.battery.status.max_charge_current_dA >> 8);
GROWATT_1AC3XXXX.data.u8[1] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
//Maximum allowable discharge current of the battery system, 0-10000 dA
GROWATT_1AC3XXXX.data.u8[2] = (datalayer.battery.status.max_discharge_current_dA >> 8);
GROWATT_1AC3XXXX.data.u8[3] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
//Maximum charging voltage of the battery system 0-15000 dV
//Stop charging when the current battery output voltage is greater than or equal to this value
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
//User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC3XXXX.data.u8[4] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[5] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF);
GROWATT_1AC3XXXX.data.u8[6] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[7] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF);
} else {
//Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC3XXXX.data.u8[4] = (datalayer.battery.info.max_design_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[5] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
GROWATT_1AC3XXXX.data.u8[6] = (datalayer.battery.info.min_design_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[7] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
}
//CV voltage value
//Total charge voltage of constant voltage (set to 10.0V below max voltage for now)
//When the battery output voltage is greater than or equal to this value, enter constant voltage charging state.
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
//User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC4XXXX.data.u8[4] = ((datalayer.battery.settings.max_user_set_charge_voltage_dV - 100) >> 8);
GROWATT_1AC4XXXX.data.u8[5] = ((datalayer.battery.settings.max_user_set_charge_voltage_dV - 100) & 0x00FF);
} else {
//Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC4XXXX.data.u8[4] = ((datalayer.battery.info.max_design_voltage_dV - 100) >> 8);
GROWATT_1AC4XXXX.data.u8[5] = ((datalayer.battery.info.max_design_voltage_dV - 100) & 0x00FF);
}
//System BMS working status
if (datalayer.battery.status.current_dA == 0) {
GROWATT_1AC5XXXX.data.u8[0] = 1; //Standby
} else if (datalayer.battery.status.current_dA < 0) { //Negative value = Discharging
GROWATT_1AC5XXXX.data.u8[0] = 3; //Discharging
} else { //Positive value = Charging
GROWATT_1AC5XXXX.data.u8[0] = 2; //Charging
}
if (datalayer.battery.status.bms_status == FAULT) {
GROWATT_1AC5XXXX.data.u8[0] = 5; //FAULT, Stop using battery!
}
//SOC 0-100 %
GROWATT_1AC6XXXX.data.u8[0] = (datalayer.battery.status.reported_soc / 100);
//SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 low SOH flag (Indicates that battery is in unsafe use)
GROWATT_1AC6XXXX.data.u8[1] = (datalayer.battery.status.soh_pptt / 100);
//Rated battery capacity when new (0-50000) dAH
GROWATT_1AC6XXXX.data.u8[2]; //TODO
GROWATT_1AC6XXXX.data.u8[3]; //TODO
//Battery voltage, 0-15000 dV
GROWATT_1AC7XXXX.data.u8[2] = (datalayer.battery.status.voltage_dV >> 8);
GROWATT_1AC7XXXX.data.u8[3] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Battery current, 0-20000dA (offset -1000A)
// Apply the -1000 offset (add 1000 to the dA value)
uint32_t current_value = (int32_t)datalayer.battery.status.current_dA + 1000;
// Clamp to uint16_t range (0 to 65535)
if (current_value < 0) {
current_value = 0;
} else if (current_value > 65535) {
current_value = 65535;
}
GROWATT_1AC7XXXX.data.u8[4] = (current_value >> 8);
GROWATT_1AC7XXXX.data.u8[5] = (current_value & 0x00FF);
}
void GrowattWitInverter::map_can_frame_to_variable(CAN_frame rx_frame) {
uint32_t first4bytes = ((rx_frame.ID & 0xFFFF0000) >> 4);
//1AB5XXXX becomes 1AB5. Most likely not needed if all PCS messages come from XXXXDFF1
switch (first4bytes) {
case 0x1AB5: // Heartbeat command, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB6: // Time and date, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB7: // PCS status information, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB8: // Bus voltage setting 1, 50ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1ABE: // PCS product information, Non-periodic
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
transmit_can_frame(&GROWATT_1AC2XXXX);
transmit_can_frame(&GROWATT_1A80XXXX);
transmit_can_frame(&GROWATT_1A82XXXX);
break;
default:
break;
}
}
void GrowattWitInverter::transmit_can(unsigned long currentMillis) {
//Send 100ms message
if (currentMillis - previousMillis100ms >= INTERVAL_100_MS) {
previousMillis100ms = currentMillis;
transmit_can_frame(&GROWATT_1AC3XXXX);
transmit_can_frame(&GROWATT_1AC4XXXX);
transmit_can_frame(&GROWATT_1AC5XXXX);
transmit_can_frame(&GROWATT_1AC7XXXX);
}
//Send 500ms message
if (currentMillis - previousMillis500ms >= INTERVAL_500_MS) {
previousMillis500ms = currentMillis;
transmit_can_frame(&GROWATT_1AC6XXXX);
}
}

View file

@ -0,0 +1,126 @@
#ifndef GROWATT_WIT_CAN_H
#define GROWATT_WIT_CAN_H
#include "CanInverterProtocol.h"
#ifdef GROWATT_WIT_CAN
#define SELECTED_INVERTER_CLASS GrowattWitInverter
#endif
class GrowattWitInverter : public CanInverterProtocol {
public:
const char* name() override { return Name; }
void update_values();
void transmit_can(unsigned long currentMillis);
void map_can_frame_to_variable(CAN_frame rx_frame);
static constexpr const char* Name = "Growatt WIT compatible battery via CAN";
private:
/* Do not change code below unless you are sure what you are doing */
//Actual content messages
CAN_frame GROWATT_1AC3XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC3, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC4XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC4, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC5XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC5, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC6XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC6, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC7XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC7, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC0XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC0, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC2XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC2, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC8XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC8, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC9XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC9, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACAXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACA, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACCXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACC, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACDXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACD, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACEXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACE, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACFXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACF, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD0XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD0, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD1XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD1, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD8XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD8, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD9XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD9, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1A80XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1A80, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1A82XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1A82, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
unsigned long previousMillis100ms = 0;
unsigned long previousMillis500ms = 0;
};
#endif

View file

@ -40,6 +40,9 @@ extern const char* name_for_inverter_type(InverterProtocolType type) {
case InverterProtocolType::GrowattLv:
return GrowattLvInverter::Name;
case InverterProtocolType::GrowattWit:
return GrowattWitInverter::Name;
case InverterProtocolType::Kostal:
return KostalInverterProtocol::Name;
@ -118,6 +121,10 @@ bool setup_inverter() {
inverter = new GrowattLvInverter();
break;
case InverterProtocolType::GrowattWit:
inverter = new GrowattWitInverter();
break;
case InverterProtocolType::Kostal:
inverter = new KostalInverterProtocol();
break;

View file

@ -18,6 +18,7 @@ extern InverterProtocol* inverter;
#include "FOXESS-CAN.h"
#include "GROWATT-HV-CAN.h"
#include "GROWATT-LV-CAN.h"
#include "GROWATT-WIT-CAN.h"
#include "KOSTAL-RS485.h"
#include "PYLON-CAN.h"
#include "PYLON-LV-CAN.h"

View file

@ -12,6 +12,7 @@ enum class InverterProtocolType {
Foxess,
GrowattHv,
GrowattLv,
GrowattWit,
Kostal,
Pylon,
PylonLv,