diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml new file mode 100644 index 000000000..c4d07e20d --- /dev/null +++ b/.github/workflows/test_build.yml @@ -0,0 +1,103 @@ +name: OpenDTU-onBattery Test Build + +on: workflow_dispatch + +jobs: + get_default_envs: + name: Gather Environments + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + + - name: Get default environments + id: envs + run: | + echo "environments=$(pio project config --json-output | jq -cr '.[1][1][0][1]|split(",")')" >> $GITHUB_OUTPUT + + outputs: + environments: ${{ steps.envs.outputs.environments }} + + build: + name: Build Enviornments + runs-on: ubuntu-latest + needs: get_default_envs + strategy: + matrix: + environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} + steps: + - uses: actions/checkout@v3 + + - name: Get tags + run: git fetch --force --tags origin + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + + - name: Setup Node.js and yarn + uses: actions/setup-node@v3 + with: + node-version: "18" + cache: "yarn" + cache-dependency-path: "webapp/yarn.lock" + + - name: Install WebApp dependencies + run: yarn --cwd webapp install --frozen-lockfile + + - name: Build WebApp + run: yarn --cwd webapp build + + - name: Build firmware + run: pio run -e ${{ matrix.environment }} + + - name: Rename Firmware + run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin + + - name: Rename Factory Firmware + run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin + + - uses: actions/upload-artifact@v3 + with: + name: opendtu-onbattery-${{ matrix.environment }} + path: | + .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin + .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin + + diff --git a/README.md b/README.md index c5f8f9162..cbd41ed9c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ designs](https://opendtu-onbattery.net/3rd_party/cases/) available for you to print yourself. ### Ready-To-Use - If you are interested in ready-to-use hardware available for sale, the OpenDTU-OnBattery project endorses the **[OpenDTU Fusion board](https://opendtu-onbattery.net/3rd_party/opendtu_fusion/)**. @@ -125,8 +124,6 @@ To find out what's new or improved have a look at the [releases](https://github.com/hoylabs/OpenDTU-OnBattery/releases). ## Project State - -OpenDTU-OnBattery is actively maintained. Please note that OpenDTU-OnBattery may change significantly during its development. Bug reports, comments, feature requests and pull requests are welcome! diff --git a/include/Configuration.h b/include/Configuration.h index bd08d2904..1a3afcc52 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -229,12 +229,19 @@ struct BATTERY_SERIAL_CONFIG_T { }; using BatterySerialConfig = struct BATTERY_SERIAL_CONFIG_T; +struct BATTERY_JKBMSCAN_CONFIG_T { + uint8_t NumberOfCells; + uint8_t CanProtocolVersion; +}; +using BatteryJkBmsCanConfig = struct BATTERY_JKBMSCAN_CONFIG_T; + struct BATTERY_CONFIG_T { bool Enabled; uint8_t Provider; BatteryMqttConfig Mqtt; BatteryZendureConfig Zendure; BatterySerialConfig Serial; + BatteryJkBmsCanConfig JkBmsCan; bool EnableDischargeCurrentLimit; float DischargeCurrentLimit; float DischargeCurrentLimitBelowSoc; @@ -486,6 +493,7 @@ class ConfigurationClass { static void serializeBatteryZendureConfig(BatteryZendureConfig const& source, JsonObject& target); static void serializeBatteryMqttConfig(BatteryMqttConfig const& source, JsonObject& target); static void serializeBatterySerialConfig(BatterySerialConfig const& source, JsonObject& target); + static void serializeBatteryJkBmsCanConfig(BatteryJkBmsCanConfig const& source, JsonObject& target); static void serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target); static void serializeGridChargerConfig(GridChargerConfig const& source, JsonObject& target); static void serializeGridChargerCanConfig(GridChargerCanConfig const& source, JsonObject& target); @@ -503,6 +511,7 @@ class ConfigurationClass { static void deserializeBatteryZendureConfig(JsonObject const& source, BatteryZendureConfig& target); static void deserializeBatteryMqttConfig(JsonObject const& source, BatteryMqttConfig& target); static void deserializeBatterySerialConfig(JsonObject const& source, BatterySerialConfig& target); + static void deserializeBatteryJkBmsCanConfig(JsonObject const& source, BatteryJkBmsCanConfig& target); static void deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target); static void deserializeGridChargerConfig(JsonObject const& source, GridChargerConfig& target); static void deserializeGridChargerCanConfig(JsonObject const& source, GridChargerCanConfig& target); diff --git a/include/PinMapping.h b/include/PinMapping.h index 3f55d121f..812b5fc0a 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -63,6 +63,7 @@ struct PinMapping_t { gpio_num_t battery_rxen; gpio_num_t battery_tx; gpio_num_t battery_txen; + uint8_t battery_can_type; gpio_num_t huawei_miso; gpio_num_t huawei_mosi; gpio_num_t huawei_clk; diff --git a/include/battery/CanReceiver.h b/include/battery/CanReceiver.h index 8c71c00fb..d3e9a5fcd 100644 --- a/include/battery/CanReceiver.h +++ b/include/battery/CanReceiver.h @@ -1,3 +1,4 @@ + // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -18,6 +19,7 @@ class CanReceiver : public Provider { protected: uint8_t readUnsignedInt8(uint8_t *data); uint16_t readUnsignedInt16(uint8_t *data); + uint16_t readBigEndianUnsignedInt16(uint8_t *data); int16_t readSignedInt16(uint8_t *data); uint32_t readUnsignedInt32(uint8_t *data); int32_t readSignedInt24(uint8_t *data); diff --git a/include/battery/jkbmscan/HassIntegration.h b/include/battery/jkbmscan/HassIntegration.h new file mode 100644 index 000000000..520f41896 --- /dev/null +++ b/include/battery/jkbmscan/HassIntegration.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +namespace Batteries::JkBmsCan { + +class HassIntegration : public ::Batteries::HassIntegration { +public: + explicit HassIntegration(std::shared_ptr spStats); + + void publishSensors() const final; +}; + +} // namespace Batteries::JkBmsCan diff --git a/include/battery/jkbmscan/Provider.h b/include/battery/jkbmscan/Provider.h new file mode 100644 index 000000000..7e73592fc --- /dev/null +++ b/include/battery/jkbmscan/Provider.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include + +namespace Batteries::JkBmsCan { + +class Provider : public ::Batteries::CanReceiver { +public: + Provider(); + bool init() final; + void onMessage(twai_message_t rx_message) final; + + std::shared_ptr<::Batteries::Stats> getStats() const final { return _stats; } + std::shared_ptr<::Batteries::HassIntegration> getHassIntegration() final { return _hassIntegration; } + +private: + void dummyData(); + + std::shared_ptr _stats; + std::shared_ptr _hassIntegration; +}; + +} // namespace Batteries::JkBmsCan diff --git a/include/battery/jkbmscan/Stats.h b/include/battery/jkbmscan/Stats.h new file mode 100644 index 000000000..a3ce04847 --- /dev/null +++ b/include/battery/jkbmscan/Stats.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace Batteries::JkBmsCan { + +class Stats : public ::Batteries::Stats { +friend class Provider; + +public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + // bool getImmediateChargingRequest() const { return _chargeImmediately; } ; + float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; + +private: + void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } + + float _chargeVoltage; + float _chargeCurrentLimitation; + float _dischargeVoltageLimitation; + uint8_t _stateOfHealth; + float _temperature; + + float _cellVoltage[25]; + float _packVoltage; + float _MaxCellVoltage; + uint8_t _MaxCellVoltageNumber; + float _MinCellVoltage; + uint8_t _MinCellVoltageNumber; + + float _capacityRemaining; + float _fullChargeCapacity; + float _cycleCapacity; + uint16_t _cycleCount; + + uint32_t _bmsRunTime; + uint16_t _heaterCurrent; + + + bool _alarmOverCurrentDischarge; + bool _alarmOverCurrentCharge; + bool _alarmUnderTemperature; + bool _alarmOverTemperature; + bool _alarmUnderVoltage; + bool _alarmOverVoltage; + bool _alarmBmsInternal; + + bool _warningHighCurrentDischarge; + bool _warningHighCurrentCharge; + bool _warningLowTemperature; + bool _warningHighTemperature; + bool _warningLowVoltage; + bool _warningHighVoltage; + bool _warningBmsInternal; + + bool _chargeEnabled; + bool _dischargeEnabled; + bool _balanceEnabled; + bool _heaterEnabled; + bool _accEnabled; + bool _chargerPluged; + + bool _chargeRequest; + bool _chargeAndHeat; + + uint8_t _moduleCount; + uint8_t _JkBmsCanVersion; +}; + +} // namespace Batteries::JkBmsCan diff --git a/include/defaults.h b/include/defaults.h index 4d08720d8..4f99c9cde 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -153,6 +153,8 @@ #define BATTERY_PROVIDER 0 // Pylontech CAN receiver #define BATTERY_SERIAL_INTERFACE 0 #define BATTERY_SERIAL_POLLING_INTERVAL 5 +#define BATTERY_NUMBER_OF_CELLS 16 // Default number of cells for a battery Pack +#define BATTERY_JKBMS_CAN_PROTOCOL_VERSION 1 // Default CAN protocol version for the JK BMS #define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false #define BATTERY_DISCHARGE_CURRENT_LIMIT 0.0 #define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC 100.0 diff --git a/platformio_override.ini b/platformio_override.ini index 00c945df0..cd54a12f3 100644 --- a/platformio_override.ini +++ b/platformio_override.ini @@ -14,8 +14,8 @@ ; Under Linux, the ports are in the format /dev/tty***, typically /dev/ttyUSB0 ; To look up the port, execute ls /dev/tty*, then connect the device ; then execute ls /dev/tty* again and check which device was added -;monitor_port = COM4 -;upload_port = COM4 +monitor_port = /dev/ttyACM2 +upload_port = /dev/ttyACM2 ; you can define your personal board and/or settings here diff --git a/src/Configuration.cpp b/src/Configuration.cpp index becff9119..3806cc11f 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -180,6 +180,12 @@ void ConfigurationClass::serializeBatterySerialConfig(BatterySerialConfig const& target["polling_interval"] = source.PollingInterval; } +void ConfigurationClass::serializeBatteryJkBmsCanConfig(BatteryJkBmsCanConfig const& source, JsonObject& target) +{ + target["number_of_cells"] = source.NumberOfCells; + target["can_protocol_version"] = source.CanProtocolVersion; +} + void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target) { char serialBuffer[sizeof(uint64_t) * 8 + 1]; @@ -436,6 +442,9 @@ bool ConfigurationClass::write() JsonObject battery_serial = battery["serial"].to(); serializeBatterySerialConfig(config.Battery.Serial, battery_serial); + + JsonObject battery_jkbmscan = battery["jkbmscan"].to(); + serializeBatteryJkBmsCanConfig(config.Battery.JkBmsCan, battery_jkbmscan); JsonObject gridcharger = doc["gridcharger"].to(); serializeGridChargerConfig(config.GridCharger, gridcharger); @@ -603,6 +612,12 @@ void ConfigurationClass::deserializeBatterySerialConfig(JsonObject const& source target.PollingInterval = source["polling_interval"] | BATTERY_SERIAL_POLLING_INTERVAL; } +void ConfigurationClass::deserializeBatteryJkBmsCanConfig(JsonObject const& source, BatteryJkBmsCanConfig& target) +{ + target.NumberOfCells = source["number_of_cells"] | BATTERY_NUMBER_OF_CELLS; + target.CanProtocolVersion = source["can_protocol_version"] | BATTERY_JKBMS_CAN_PROTOCOL_VERSION; +} + void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target) { auto serialBin = [](String const& input) -> uint64_t { @@ -884,6 +899,7 @@ bool ConfigurationClass::read() deserializeBatteryZendureConfig(battery["zendure"], config.Battery.Zendure); deserializeBatteryMqttConfig(battery["mqtt"], config.Battery.Mqtt); deserializeBatterySerialConfig(battery["serial"], config.Battery.Serial); + deserializeBatteryJkBmsCanConfig(battery["jkbmscan"], config.Battery.JkBmsCan); JsonObject gridcharger = doc["gridcharger"]; deserializeGridChargerConfig(gridcharger, config.GridCharger); diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 76a100719..b34196efe 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -168,6 +168,10 @@ static const char* TAG = "pinmapping"; #define BATTERY_PIN_RX GPIO_NUM_NC #endif +#ifndef BATTERY_CAN_TYPE +#define BATTERY_CAN_TYPE 1 +#endif + #ifdef PYLONTECH_PIN_RX #undef BATTERY_PIN_RX #define BATTERY_PIN_RX PYLONTECH_PIN_RX @@ -305,6 +309,7 @@ PinMappingClass::PinMappingClass() _pinMapping.battery_rxen = BATTERY_PIN_RXEN; _pinMapping.battery_tx = BATTERY_PIN_TX; _pinMapping.battery_txen = BATTERY_PIN_TXEN; + _pinMapping.battery_can_type = BATTERY_CAN_TYPE; _pinMapping.huawei_miso = HUAWEI_PIN_MISO; _pinMapping.huawei_mosi = HUAWEI_PIN_MOSI; @@ -415,6 +420,7 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; _pinMapping.battery_tx = doc[i]["battery"]["tx"] | BATTERY_PIN_TX; _pinMapping.battery_txen = doc[i]["battery"]["txen"] | BATTERY_PIN_TXEN; + _pinMapping.battery_can_type = doc[i]["battery"]["can_type"] | BATTERY_CAN_TYPE; _pinMapping.huawei_miso = doc[i]["huawei"]["miso"] | HUAWEI_PIN_MISO; _pinMapping.huawei_mosi = doc[i]["huawei"]["mosi"] | HUAWEI_PIN_MOSI; diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 7981366da..0182d075b 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -45,6 +45,9 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) auto serial = root["serial"].to(); ConfigurationClass::serializeBatterySerialConfig(config.Battery.Serial, serial); + auto jkbmscan = root["jkbmscan"].to(); + ConfigurationClass::serializeBatteryJkBmsCanConfig(config.Battery.JkBmsCan, jkbmscan); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -85,6 +88,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) ConfigurationClass::deserializeBatteryZendureConfig(root["zendure"].as(), config.Battery.Zendure); ConfigurationClass::deserializeBatteryMqttConfig(root["mqtt"].as(), config.Battery.Mqtt); ConfigurationClass::deserializeBatterySerialConfig(root["serial"].as(), config.Battery.Serial); + ConfigurationClass::deserializeBatteryJkBmsCanConfig(root["jkbmscan"].as(), config.Battery.JkBmsCan); } WebApi.writeConfig(retMsg); diff --git a/src/battery/CanReceiver.cpp b/src/battery/CanReceiver.cpp index ec353755b..54bd3d76e 100644 --- a/src/battery/CanReceiver.cpp +++ b/src/battery/CanReceiver.cpp @@ -33,9 +33,17 @@ bool CanReceiver::init(char const* providerName) // esp_intr_dump() function, but that's not available yet in our version // of the underlying esp-idf. g_config.intr_flags = ESP_INTR_FLAG_LEVEL2; - + twai_timing_config_t t_config; // Initialize configuration structures using macro initializers - twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); + if(pin.battery_can_type==2) { + t_config = TWAI_TIMING_CONFIG_250KBITS(); + DTU_LOGD("Twai driver set to 250 KBITS"); + } + else + { + t_config = TWAI_TIMING_CONFIG_500KBITS(); + DTU_LOGD("Twai driver set to 500 KBITS"); + } twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); // Install TWAI driver @@ -128,7 +136,6 @@ void CanReceiver::loop() DTU_LOGD("Received CAN message: 0x%04X (%d bytes)", rx_message.identifier, rx_message.data_length_code); LogHelper::dumpBytes(TAG, _providerName, rx_message.data, rx_message.data_length_code); - onMessage(rx_message); } @@ -142,6 +149,11 @@ uint16_t CanReceiver::readUnsignedInt16(uint8_t *data) return (data[1] << 8) | data[0]; } +uint16_t CanReceiver::readBigEndianUnsignedInt16(uint8_t *data) +{ + return (data[0] << 8) | data[1]; +} + int16_t CanReceiver::readSignedInt16(uint8_t *data) { return this->readUnsignedInt16(data); diff --git a/src/battery/Controller.cpp b/src/battery/Controller.cpp index 988c4d676..8bdcb56d5 100644 --- a/src/battery/Controller.cpp +++ b/src/battery/Controller.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -77,6 +78,9 @@ void Controller::updateSettings() case 7: _upProvider = std::make_unique(); break; + case 8: + _upProvider = std::make_unique(); + break; default: DTU_LOGE("Unknown provider: %d", config.Battery.Provider); return; diff --git a/src/battery/jkbmscan/HassIntegration.cpp b/src/battery/jkbmscan/HassIntegration.cpp new file mode 100644 index 000000000..e9dc90170 --- /dev/null +++ b/src/battery/jkbmscan/HassIntegration.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +namespace Batteries::JkBmsCan { + +HassIntegration::HassIntegration(std::shared_ptr spStats) + : ::Batteries::HassIntegration(spStats) { } + +void HassIntegration::publishSensors() const +{ + ::Batteries::HassIntegration::publishSensors(); + uint8_t i; + auto const& config = Configuration.get(); + + publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C"); + publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%"); + publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V"); + publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge voltage limit", NULL, "settings/dischargeVoltageLimitation", "voltage", "measurement", "V"); + publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Module Count", "mdi:counter", "modulesTotal"); + + publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0"); + publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0"); + + publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0"); + publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0"); + + publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0"); + publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0"); + + publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0"); + publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0"); + + publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0"); + publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0"); + + publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0"); + publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0"); + + publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0"); + publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0"); + + publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0"); + publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0"); + publishBinarySensor("Charge immediately", "mdi:alert", "charging/chargeImmediately", "1", "0"); + + String cellno; + //char str[4]; + String str; + for (i=0; i99) + { + i=99; + } + if (i<10) + { + cellno="battery/Cell0"+str+"Voltage"; + //cellno.concat("battery/Cell0"); + //cellno.concat(str); + //cellno.concat("Voltage"); + } + else + { + cellno="battery/Cell0"+str+"Voltage"; + //cellno.concat("battery/Cell"); + //cellno.concat(str); + //cellno.concat("Voltage"); + } + publishSensor(cellno.c_str(), NULL, cellno.c_str(), "voltage", "measurement", "mV"); + } + + + + + +} + +} // namespace Batteries::JkBmsCan diff --git a/src/battery/jkbmscan/Provider.cpp b/src/battery/jkbmscan/Provider.cpp new file mode 100644 index 000000000..f202b47f3 --- /dev/null +++ b/src/battery/jkbmscan/Provider.cpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include +#include +#include + +static const char* TAG = "battery"; +static const char* SUBTAG = "JkBmsCan"; + +namespace Batteries::JkBmsCan { + +Provider::Provider() + : _stats(std::make_shared()) + , _hassIntegration(std::make_shared(_stats)) { } + +bool Provider::init() +{ + return ::Batteries::CanReceiver::init("JkBmsCan"); +} + +void Provider::onMessage(twai_message_t rx_message) +{ + auto const& config = Configuration.get(); + switch (rx_message.identifier) { + case 0x02F4: { + // We use the reported Voltage with Can Protocol Version 1 otherwise we use the calculated voltage from single cell voltages because it seems more accurate. + if (config.Battery.JkBmsCan.CanProtocolVersion == 1) + { + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.1), millis()); + } + else + { + _stats->_packVoltage=0; + for(int i=0; i_packVoltage+=_stats->_cellVoltage[i]; + } + _stats->setVoltage(this->scaleValue((_stats->_packVoltage ), 0.001), millis()); + } + _stats->setCurrent((this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1)-400.0), 1/*precision*/, millis()); + _stats->setSoC(static_cast(this->readUnsignedInt8(rx_message.data + 4)), 0/*precision*/, millis()); + String manufacturer = "JKBMS ID: 0"; + if (manufacturer.isEmpty()) { break; } + + DTU_LOGD("[JkBmsCan] Manufacturer: %s\r\n", manufacturer.c_str()); + + _stats->setManufacturer(manufacturer); + break; + } + case 0x04F4: { + _stats->_MaxCellVoltage=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_MaxCellVoltageNumber=(static_cast(this->readUnsignedInt8(rx_message.data+2))); + _stats->_MinCellVoltage=(static_cast(this->readUnsignedInt16(rx_message.data+3))); + _stats->_MinCellVoltageNumber=(static_cast(this->readUnsignedInt8(rx_message.data+5))); + break; + } + + case 0x05F4: { + _stats->_temperature = (static_cast(this->readUnsignedInt8(rx_message.data + 4))) - 50.0; + DTU_LOGD("[JkBmsCan] voltage: %f current: %f temperature: %f", + _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); + + break; + } + + + case 0x18E028F4: { + _stats->_cellVoltage[0]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[1]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[2]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[3]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + + case 0x18E128F4: { + _stats->_cellVoltage[4]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[5]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[6]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[7]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + + case 0x18E228F4: { + _stats->_cellVoltage[8]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[9]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[10]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[11]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + case 0x18E328F4: { + _stats->_cellVoltage[12]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[13]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[14]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[15]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + case 0x18E428F4: { + _stats->_cellVoltage[16]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[17]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[18]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[19]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + case 0x18E528F4: { + _stats->_cellVoltage[20]=(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->_cellVoltage[21]=(static_cast(this->readUnsignedInt16(rx_message.data+2))); + _stats->_cellVoltage[22]=(static_cast(this->readUnsignedInt16(rx_message.data+4))); + _stats->_cellVoltage[23]=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + case 0x18E628F4: { + _stats->_cellVoltage[24]=(static_cast(this->readUnsignedInt16(rx_message.data))); + break; + } + + case 0x18F128F4: { + _stats->_capacityRemaining = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); + _stats->_fullChargeCapacity = this->scaleValue(this->readUnsignedInt16(rx_message.data+2), 0.1); + _stats->_cycleCapacity = this->scaleValue(this->readUnsignedInt16(rx_message.data+4), 0.1); + _stats->_cycleCount=(static_cast(this->readUnsignedInt16(rx_message.data+6))); + break; + } + + case 0x18F328F4: { + uint16_t alarmBits = rx_message.data[0]; + _stats->_alarmOverCurrentDischarge = this->getBit(alarmBits, 7); + _stats->_alarmUnderTemperature = this->getBit(alarmBits, 4); + _stats->_alarmOverTemperature = this->getBit(alarmBits, 3); + _stats->_alarmUnderVoltage = this->getBit(alarmBits, 2); + _stats->_alarmOverVoltage= this->getBit(alarmBits, 1); + + alarmBits = rx_message.data[1]; + _stats->_alarmBmsInternal= this->getBit(alarmBits, 3); + _stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0); + + DTU_LOGD("[JkBmsCan] Alarms: %d %d %d %d %d %d %d", + _stats->_alarmOverCurrentDischarge, + _stats->_alarmUnderTemperature, + _stats->_alarmOverTemperature, + _stats->_alarmUnderVoltage, + _stats->_alarmOverVoltage, + _stats->_alarmBmsInternal, + _stats->_alarmOverCurrentCharge); + + + uint16_t warningBits = rx_message.data[2]; + _stats->_warningHighCurrentDischarge = this->getBit(warningBits, 7); + _stats->_warningLowTemperature = this->getBit(warningBits, 4); + _stats->_warningHighTemperature = this->getBit(warningBits, 3); + _stats->_warningLowVoltage = this->getBit(warningBits, 2); + _stats->_warningHighVoltage = this->getBit(warningBits, 1); + + warningBits = rx_message.data[3]; + _stats->_warningBmsInternal= this->getBit(warningBits, 3); + _stats->_warningHighCurrentCharge = this->getBit(warningBits, 0); + + DTU_LOGD("[JkBmsCan] Warnings: %d %d %d %d %d %d %d", + _stats->_warningHighCurrentDischarge, + _stats->_warningLowTemperature, + _stats->_warningHighTemperature, + _stats->_warningLowVoltage, + _stats->_warningHighVoltage, + _stats->_warningBmsInternal, + _stats->_warningHighCurrentCharge); + break; + } + + case 0x18F428F4: { + _stats->_bmsRunTime = this->readUnsignedInt32(rx_message.data); + _stats->_heaterCurrent = this->readUnsignedInt16(rx_message.data + 4); + _stats->_stateOfHealth = this->readUnsignedInt8(rx_message.data + 6); + break; + } + + case 0x18F528F4: { + uint16_t chargeStatusBits = rx_message.data[0]; + _stats->_chargeEnabled = this->getBit(chargeStatusBits, 0); + _stats->_dischargeEnabled = this->getBit(chargeStatusBits, 1); + _stats->_balanceEnabled = this->getBit(chargeStatusBits, 2); + _stats->_heaterEnabled = this->getBit(chargeStatusBits, 3); + _stats->_chargerPluged = this->getBit(chargeStatusBits, 4); + _stats->_accEnabled = this->getBit(chargeStatusBits, 5); + + DTU_LOGD("[JkBmsCan] chargeStatusBits: %d %d %d", + _stats->_chargeEnabled, + _stats->_dischargeEnabled, + _stats->_balanceEnabled); + + break; + } + + case 0x1806E5F4: { + _stats->_chargeVoltage = this->scaleValue(this->readBigEndianUnsignedInt16(rx_message.data), 0.1); + _stats->_chargeCurrentLimitation = this->scaleValue(this->readBigEndianUnsignedInt16(rx_message.data + 2), 0.1); + _stats->_chargeRequest = this->readUnsignedInt8(rx_message.data + 4); + _stats->_chargeAndHeat = this->readUnsignedInt8(rx_message.data + 5); + + + + DTU_LOGD("[JkBmsCan] chargeVoltage: %f chargeCurrentLimitation: %f chargeRequest: %d chargeAndHeat: %d\r\n", + _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_chargeRequest, + _stats->_chargeAndHeat); + break; + } + + default: + return; // do not update last update timestamp + break; + } + + _stats->setLastUpdate(millis()); +} + + + +} // namespace Batteries::JkBmsCan diff --git a/src/battery/jkbmscan/Stats.cpp b/src/battery/jkbmscan/Stats.cpp new file mode 100644 index 000000000..b7b2cec06 --- /dev/null +++ b/src/battery/jkbmscan/Stats.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include + +namespace Batteries::JkBmsCan { + +void Stats::getLiveViewData(JsonVariant& root) const +{ + ::Batteries::Stats::getLiveViewData(root); + auto const& config = Configuration.get(); + uint8_t i; + + // values go into the "Status" card of the web application + addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1); + addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); + addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimitation, "V", 1); + addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); + addLiveViewValue(root, "temperature", _temperature, "°C", 1); + addLiveViewValue(root, "modules", _moduleCount, "", 0); + std::string cellno; + for (i=0; i99) + { + i=99; + } + if (i<10) + { + cellno="battery/Cell0"+str+"Voltage"; + //cellno.concat("battery/Cell0"); + //cellno.concat(str); + //cellno.concat("Voltage"); + } + else + { + cellno="battery/Cell0"+str+"Voltage"; + //cellno.concat("battery/Cell"); + //cellno.concat(str); + //cellno.concat("Voltage"); + } + MqttSettings.publish(cellno, String(_cellVoltage[i])); + + } + + + +} + +} // namespace Batteries::JkBmsCan diff --git a/webapp/package.json b/webapp/package.json index 0ebafbec8..a9bbe4cd0 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -45,6 +45,7 @@ "vite": "^6.3.5", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-vue-devtools": "^7.7.6", "vue-tsc": "^2.2.10" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 9dfff0f32..da7053134 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -763,6 +763,7 @@ "ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle", "ProviderPytesCan": "Pytes per CAN-Bus", "ProviderZendureMqtt": "Zendure per lokalem MQTT Broker", + "ProviderJkBmsCan": "Jikong (JK) BMS per CAN-Bus", "MqttSocConfiguration": "Einstellungen SoC", "MqttVoltageConfiguration": "Einstellungen Spannung", "MqttCurrentConfiguration": "Einstellungen Strom", @@ -777,6 +778,8 @@ "SerialInterfaceTypeUart": "TTL-UART an der MCU", "SerialInterfaceTypeTransceiver": "RS-485 Transceiver an der MCU", "JbdBmsConfiguration": "JBD BMS Einstellungen", + "JkBmsPackConfiguration": "JK BMS Pack Konfiguration", + "NumberOfCells": "Anzahl der Zellen im Pack", "PollingInterval": "Abfrageintervall", "Seconds": "@:base.Seconds", "DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index cfa631a25..40f198ddc 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -763,6 +763,7 @@ "ProviderVictron": "Victron SmartShunt using VE.Direct interface", "ProviderPytesCan": "Pytes using CAN bus", "ProviderZendureMqtt": "Zendure using local MQTT broker", + "ProviderJkBmsCan": "Jikong (JK) BMS using CAN-Bus", "MqttConfiguration": "MQTT Settings", "MqttSocConfiguration": "SoC Settings", "MqttVoltageConfiguration": "Voltage Settings", @@ -778,6 +779,8 @@ "SerialInterfaceTypeUart": "TTL-UART on MCU", "SerialInterfaceTypeTransceiver": "RS-485 Transceiver on MCU", "JbdBmsConfiguration": "JBD BMS Settings", + "JkBmsPackConfiguration": "JK BMS pack configuration", + "NumberOfCells": "Number of Cells in this battery pack", "PollingInterval": "Polling Interval", "Seconds": "@:base.Seconds", "DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 6fd3c4e30..8fba7d36a 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -595,6 +595,7 @@ "ProviderMqtt": "Battery data from MQTT broker", "ProviderVictron": "Victron SmartShunt using VE.Direct interface", "ProviderZendureMqtt": "Zendure using local MQTT broker", + "ProviderJkBmsCan": "Jikong (JK) BMS using CAN-Bus", "MqttSocConfiguration": "SoC Settings", "MqttVoltageConfiguration": "Voltage Settings", "MqttJsonPath": "Optional: JSON Path", @@ -607,6 +608,8 @@ "SerialInterfaceTypeUart": "TTL-UART on MCU", "SerialInterfaceTypeTransceiver": "RS-485 Transceiver on MCU", "JbdBmsConfiguration": "JBD BMS Settings", + "JkBmsPackConfiguration": "JK BMS pack configuration", + "NumberOfCells": "Number of Cells in this battery pack", "PollingInterval": "Polling Interval", "Seconds": "@:base.Seconds", "DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings", diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts index abf46ca45..b0e9d99bc 100644 --- a/webapp/src/types/BatteryConfig.ts +++ b/webapp/src/types/BatteryConfig.ts @@ -38,12 +38,19 @@ export interface BatterySerialConfig { polling_interval: number; } +export interface BatteryJkBmsCanConfig { + number_of_cells: number; + can_protocol_version: number; + +} + export interface BatteryConfig { enabled: boolean; provider: number; serial: BatterySerialConfig; mqtt: BatteryMqttConfig; zendure: BatteryZendureConfig; + jkbmscan: BatteryJkBmsCanConfig; enable_discharge_current_limit: boolean; discharge_current_limit: number; discharge_current_limit_below_soc: number; diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index dbfd7dc2e..c899a1e79 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -69,6 +69,32 @@ /> + + + + +