mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-04 02:09:30 +02:00
Merge pull request #1367 from dalathegreat/feature/growatt-WIT
New Inverter ⚡ Add initial support for Growatt Wit CAN
This commit is contained in:
commit
27f95c5388
5 changed files with 289 additions and 0 deletions
154
Software/src/inverter/GROWATT-WIT-CAN.cpp
Normal file
154
Software/src/inverter/GROWATT-WIT-CAN.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
126
Software/src/inverter/GROWATT-WIT-CAN.h
Normal file
126
Software/src/inverter/GROWATT-WIT-CAN.h
Normal 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
|
|
@ -40,6 +40,9 @@ extern const char* name_for_inverter_type(InverterProtocolType type) {
|
||||||
case InverterProtocolType::GrowattLv:
|
case InverterProtocolType::GrowattLv:
|
||||||
return GrowattLvInverter::Name;
|
return GrowattLvInverter::Name;
|
||||||
|
|
||||||
|
case InverterProtocolType::GrowattWit:
|
||||||
|
return GrowattWitInverter::Name;
|
||||||
|
|
||||||
case InverterProtocolType::Kostal:
|
case InverterProtocolType::Kostal:
|
||||||
return KostalInverterProtocol::Name;
|
return KostalInverterProtocol::Name;
|
||||||
|
|
||||||
|
@ -118,6 +121,10 @@ bool setup_inverter() {
|
||||||
inverter = new GrowattLvInverter();
|
inverter = new GrowattLvInverter();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case InverterProtocolType::GrowattWit:
|
||||||
|
inverter = new GrowattWitInverter();
|
||||||
|
break;
|
||||||
|
|
||||||
case InverterProtocolType::Kostal:
|
case InverterProtocolType::Kostal:
|
||||||
inverter = new KostalInverterProtocol();
|
inverter = new KostalInverterProtocol();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -18,6 +18,7 @@ extern InverterProtocol* inverter;
|
||||||
#include "FOXESS-CAN.h"
|
#include "FOXESS-CAN.h"
|
||||||
#include "GROWATT-HV-CAN.h"
|
#include "GROWATT-HV-CAN.h"
|
||||||
#include "GROWATT-LV-CAN.h"
|
#include "GROWATT-LV-CAN.h"
|
||||||
|
#include "GROWATT-WIT-CAN.h"
|
||||||
#include "KOSTAL-RS485.h"
|
#include "KOSTAL-RS485.h"
|
||||||
#include "PYLON-CAN.h"
|
#include "PYLON-CAN.h"
|
||||||
#include "PYLON-LV-CAN.h"
|
#include "PYLON-LV-CAN.h"
|
||||||
|
|
|
@ -12,6 +12,7 @@ enum class InverterProtocolType {
|
||||||
Foxess,
|
Foxess,
|
||||||
GrowattHv,
|
GrowattHv,
|
||||||
GrowattLv,
|
GrowattLv,
|
||||||
|
GrowattWit,
|
||||||
Kostal,
|
Kostal,
|
||||||
Pylon,
|
Pylon,
|
||||||
PylonLv,
|
PylonLv,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue