From 9891b015563fd4a2cee83f914d8699fa8f062d98 Mon Sep 17 00:00:00 2001 From: Willem Bleymueller Date: Fri, 11 Jul 2025 00:46:55 +0200 Subject: [PATCH 01/10] Initial CAN Driver for JK BMS The driver was cloned from the pylontech driver with some modifications for the hardware interface. The CAN Interface can be configured for 250 kBits CAN Speed by adding the statement "can_type": 2 to the pin description in the hardware config. "battery": { "rx": 13, "tx": 14, "can_type": 2 } sets the speed to 250 kBits. Any other value will set it to 500 kBits which was previously the default. --- include/PinMapping.h | 1 + include/battery/CanReceiver.h | 1 + include/battery/jkbmscan/HassIntegration.h | 16 ++ include/battery/jkbmscan/Provider.h | 28 +++ include/battery/jkbmscan/Stats.h | 49 ++++ platformio_override.ini | 4 +- src/PinMapping.cpp | 6 + src/battery/CanReceiver.cpp | 15 +- src/battery/Controller.cpp | 4 + src/battery/jkbmscan/HassIntegration.cpp | 48 ++++ src/battery/jkbmscan/Provider.cpp | 246 +++++++++++++++++++++ src/battery/jkbmscan/Stats.cpp | 75 +++++++ webapp/src/views/BatteryAdminView.vue | 4 +- 13 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 include/battery/jkbmscan/HassIntegration.h create mode 100644 include/battery/jkbmscan/Provider.h create mode 100644 include/battery/jkbmscan/Stats.h create mode 100644 src/battery/jkbmscan/HassIntegration.cpp create mode 100644 src/battery/jkbmscan/Provider.cpp create mode 100644 src/battery/jkbmscan/Stats.cpp 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..e8cf335f5 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 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..daebc32f7 --- /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(bool verboseLogging) 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..43ed1bcdb --- /dev/null +++ b/include/battery/jkbmscan/Stats.h @@ -0,0 +1,49 @@ +// 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; + uint16_t _stateOfHealth; + float _temperature; + + 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 _chargeImmediately; + + uint8_t _moduleCount; +}; + +} // namespace Batteries::JkBmsCan 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/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/battery/CanReceiver.cpp b/src/battery/CanReceiver.cpp index ec353755b..51cdc7e3c 100644 --- a/src/battery/CanReceiver.cpp +++ b/src/battery/CanReceiver.cpp @@ -33,9 +33,19 @@ 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(); + MessageOutput.printf("[%s] Twai driver set to 250 KBITS\r\n", + _providerName); + } + else + { + t_config = TWAI_TIMING_CONFIG_500KBITS(); + MessageOutput.printf("[%s] Twai driver set to 500 KBITS\r\n", + _providerName); + } twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); // Install TWAI driver @@ -128,7 +138,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); } 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..4fdefd974 --- /dev/null +++ b/src/battery/jkbmscan/HassIntegration.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +namespace Batteries::JkBmsCan { + +HassIntegration::HassIntegration(std::shared_ptr spStats) + : ::Batteries::HassIntegration(spStats) { } + +void HassIntegration::publishSensors() const +{ + ::Batteries::HassIntegration::publishSensors(); + + 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"); +} + +} // namespace Batteries::JkBmsCan diff --git a/src/battery/jkbmscan/Provider.cpp b/src/battery/jkbmscan/Provider.cpp new file mode 100644 index 000000000..ac74d2143 --- /dev/null +++ b/src/battery/jkbmscan/Provider.cpp @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include + +namespace Batteries::JkBmsCan { + +Provider::Provider() + : _stats(std::make_shared()) + , _hassIntegration(std::make_shared(_stats)) { } + +bool Provider::init(bool verboseLogging) +{ + return ::Batteries::CanReceiver::init(verboseLogging, "JkBmsCan"); +} + +void Provider::onMessage(twai_message_t rx_message) +{ + switch (rx_message.identifier) { + case 0x351: { + _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); + _stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); + _stats->setDischargeCurrentLimit(this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1), millis()); + _stats->_dischargeVoltageLimitation = this->scaleValue(this->readUnsignedInt16(rx_message.data + 6), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f dischargeVoltageLimitation: %f\r\n", + _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit(), + _stats->_dischargeVoltageLimitation); + } + break; + } + + case 0x355: { + _stats->setSoC(static_cast(this->readUnsignedInt16(rx_message.data)), 0/*precision*/, millis()); + _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] soc: %f soh: %d\r\n", + _stats->getSoC(), _stats->_stateOfHealth); + } + break; + } + case 0x02F4: { + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.1), 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()); + // _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + break; + } + case 0x05F4: { + _stats->_temperature = (static_cast(this->readUnsignedInt8(rx_message.data + 4))) - 50.0; + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] voltage: %f current: %f temperature: %f\r\n", + _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); + } + break; + } + case 0x356: { + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); + _stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis()); + _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] voltage: %f current: %f temperature: %f\r\n", + _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); + } + break; + } + + case 0x359: { + 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); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] Alarms: %d %d %d %d %d %d %d\r\n", + _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); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] Warnings: %d %d %d %d %d %d %d\r\n", + _stats->_warningHighCurrentDischarge, + _stats->_warningLowTemperature, + _stats->_warningHighTemperature, + _stats->_warningLowVoltage, + _stats->_warningHighVoltage, + _stats->_warningBmsInternal, + _stats->_warningHighCurrentCharge); + } + + _stats->_moduleCount = rx_message.data[4]; + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] Modules: %d\r\n", + _stats->_moduleCount); + } + + break; + } + + case 0x35E: { + String manufacturer(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (manufacturer.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] Manufacturer: %s\r\n", manufacturer.c_str()); + } + + _stats->setManufacturer(manufacturer); + break; + } + + case 0x35C: { + uint16_t chargeStatusBits = rx_message.data[0]; + _stats->_chargeEnabled = this->getBit(chargeStatusBits, 7); + _stats->_dischargeEnabled = this->getBit(chargeStatusBits, 6); + _stats->_chargeImmediately = this->getBit(chargeStatusBits, 5); + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] chargeStatusBits: %d %d %d\r\n", + _stats->_chargeEnabled, + _stats->_dischargeEnabled, + _stats->_chargeImmediately); + } + + break; + } + + default: + return; // do not update last update timestamp + break; + } + + _stats->setLastUpdate(millis()); +} + +// Currently not called because there is no nice way to integrate it right now +#ifdef PYLONTECH_DUMMY +void Provider::dummyData() +{ + static uint32_t lastUpdate = millis(); + static uint8_t issues = 0; + + if (millis() < (lastUpdate + 5 * 1000)) { return; } + + lastUpdate = millis(); + _stats->setLastUpdate(lastUpdate); + + auto dummyFloat = [](int offset) -> float { + return offset + (static_cast((lastUpdate + offset) % 10) / 10); + }; + + _stats->setManufacturer("JkBmsCan US3000C"); + _stats->setSoC(42, 0/*precision*/, millis()); + _stats->_chargeVoltage = dummyFloat(50); + _stats->_chargeCurrentLimitation = dummyFloat(33); + _stats->setDischargeCurrentLimit(dummyFloat(12), millis()); + _stats->_dischargeVoltageLimitation = dummyFloat(46); + _stats->_stateOfHealth = 99; + _stats->setVoltage(48.67, millis()); + _stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis()); + _stats->_temperature = dummyFloat(20); + + _stats->_chargeEnabled = true; + _stats->_dischargeEnabled = true; + _stats->_chargeImmediately = false; + + _stats->_moduleCount = 1; + + _stats->_warningHighCurrentDischarge = false; + _stats->_warningHighCurrentCharge = false; + _stats->_warningLowTemperature = false; + _stats->_warningHighTemperature = false; + _stats->_warningLowVoltage = false; + _stats->_warningHighVoltage = false; + _stats->_warningBmsInternal = false; + + _stats->_alarmOverCurrentDischarge = false; + _stats->_alarmOverCurrentCharge = false; + _stats->_alarmUnderTemperature = false; + _stats->_alarmOverTemperature = false; + _stats->_alarmUnderVoltage = false; + _stats->_alarmOverVoltage = false; + _stats->_alarmBmsInternal = false; + + if (issues == 1 || issues == 3) { + _stats->_warningHighCurrentDischarge = true; + _stats->_warningHighCurrentCharge = true; + _stats->_warningLowTemperature = true; + _stats->_warningHighTemperature = true; + _stats->_warningLowVoltage = true; + _stats->_warningHighVoltage = true; + _stats->_warningBmsInternal = true; + } + + if (issues == 2 || issues == 3) { + _stats->_alarmOverCurrentDischarge = true; + _stats->_alarmOverCurrentCharge = true; + _stats->_alarmUnderTemperature = true; + _stats->_alarmOverTemperature = true; + _stats->_alarmUnderVoltage = true; + _stats->_alarmOverVoltage = true; + _stats->_alarmBmsInternal = true; + } + + if (issues == 4) { + _stats->_warningHighCurrentCharge = true; + _stats->_warningLowTemperature = true; + _stats->_alarmUnderVoltage = true; + _stats->_dischargeEnabled = false; + _stats->_chargeImmediately = true; + } + + issues = (issues + 1) % 5; +} +#endif + +} // namespace Batteries::JkBmsCan diff --git a/src/battery/jkbmscan/Stats.cpp b/src/battery/jkbmscan/Stats.cpp new file mode 100644 index 000000000..17eacafc3 --- /dev/null +++ b/src/battery/jkbmscan/Stats.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + +namespace Batteries::JkBmsCan { + +void Stats::getLiveViewData(JsonVariant& root) const +{ + ::Batteries::Stats::getLiveViewData(root); + + // 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); + + addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no")); + addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no")); + addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no")); + + // alarms and warnings go into the "Issues" card of the web application + addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge); + addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge); + + addLiveViewWarning(root, "highCurrentCharge", _warningHighCurrentCharge); + addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge); + + addLiveViewWarning(root, "lowTemperature", _warningLowTemperature); + addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); + + addLiveViewWarning(root, "highTemperature", _warningHighTemperature); + addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); + + addLiveViewWarning(root, "lowVoltage", _warningLowVoltage); + addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); + + addLiveViewWarning(root, "highVoltage", _warningHighVoltage); + addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); + + addLiveViewWarning(root, "bmsInternal", _warningBmsInternal); + addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal); +} + +void Stats::mqttPublish() const +{ + ::Batteries::Stats::mqttPublish(); + + MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); + MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); + MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimitation)); + MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); + MqttSettings.publish("battery/temperature", String(_temperature)); + MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); + MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); + MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); + MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); + MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); + MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); + MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal)); + MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge)); + MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge)); + MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); + MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); + MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); + MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); + MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal)); + MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled)); + MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled)); + MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); + MqttSettings.publish("battery/modulesTotal", String(_moduleCount)); +} + +} // namespace Batteries::JkBmsCan diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index dbfd7dc2e..e32ac1dc7 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -217,7 +217,8 @@ (batteryConfigList.provider == 0 || batteryConfigList.provider == 2 || batteryConfigList.provider == 4 || - batteryConfigList.provider == 5) + batteryConfigList.provider == 5 || + batteryConfigList.provider == 8) " > Date: Wed, 21 May 2025 11:01:35 +0200 Subject: [PATCH 02/10] Adding translations for the Web interface in the battery admin module. --- src/battery/jkbmscan/Provider.cpp | 24 ++++++++++++------------ webapp/src/locales/de.json | 1 + webapp/src/locales/en.json | 1 + webapp/src/locales/fr.json | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/battery/jkbmscan/Provider.cpp b/src/battery/jkbmscan/Provider.cpp index ac74d2143..720f59b03 100644 --- a/src/battery/jkbmscan/Provider.cpp +++ b/src/battery/jkbmscan/Provider.cpp @@ -48,6 +48,14 @@ void Provider::onMessage(twai_message_t rx_message) _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()); // _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + String manufacturer = "JKBMS ID: 0"; + if (manufacturer.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[JkBmsCan] Manufacturer: %s\r\n", manufacturer.c_str()); + } + + _stats->setManufacturer(manufacturer); break; } case 0x05F4: { @@ -58,6 +66,10 @@ void Provider::onMessage(twai_message_t rx_message) } break; } + + case 0x18F128F4: { + break; + } case 0x356: { _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); _stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis()); @@ -124,19 +136,7 @@ void Provider::onMessage(twai_message_t rx_message) break; } - case 0x35E: { - String manufacturer(reinterpret_cast(rx_message.data), - rx_message.data_length_code); - if (manufacturer.isEmpty()) { break; } - - if (_verboseLogging) { - MessageOutput.printf("[JkBmsCan] Manufacturer: %s\r\n", manufacturer.c_str()); - } - - _stats->setManufacturer(manufacturer); - break; - } case 0x35C: { uint16_t chargeStatusBits = rx_message.data[0]; diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index c7ce8268c..60ae8feb1 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -761,6 +761,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", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 9e04c6f51..682daf011 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -761,6 +761,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", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 47fb72454..7a93e8c51 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", From 082baeedbc14f2040b91712bae44bdf1ee440dac Mon Sep 17 00:00:00 2001 From: Willem Bleymueller Date: Mon, 26 May 2025 00:30:08 +0200 Subject: [PATCH 03/10] Adding parameter number_of_cells to battery admin menu The number of cells for the battery pack can be configured. Then the voltage of the individual cells will is displayed in the overview. Implemented most JK BMS Can Identifiers. Cleaning up the code. Removed some leftover code from the originating pylontech driver. --- include/Configuration.h | 8 + include/battery/jkbmscan/Stats.h | 27 +- include/defaults.h | 1 + src/Configuration.cpp | 14 + src/WebApi_battery.cpp | 4 + src/battery/jkbmscan/HassIntegration.cpp | 35 + src/battery/jkbmscan/Provider.cpp | 218 +++---- src/battery/jkbmscan/Stats.cpp | 79 ++- webapp/package.json | 1 + webapp/src/locales/de.json | 2 + webapp/src/locales/en.json | 2 + webapp/src/locales/fr.json | 2 + webapp/src/types/BatteryConfig.ts | 5 + webapp/src/views/BatteryAdminView.vue | 19 +- webapp/vite.config.ts | 3 + webapp/yarn.lock | 784 +++++++++++++++++++++++ 16 files changed, 1070 insertions(+), 134 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 734a3c7e8..6e03a6901 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -228,12 +228,18 @@ struct BATTERY_SERIAL_CONFIG_T { }; using BatterySerialConfig = struct BATTERY_SERIAL_CONFIG_T; +struct BATTERY_JKBMSCAN_CONFIG_T { + uint8_t number_of_cells; +}; +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; @@ -485,6 +491,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); @@ -502,6 +509,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/battery/jkbmscan/Stats.h b/include/battery/jkbmscan/Stats.h index 43ed1bcdb..f9d7c4daa 100644 --- a/include/battery/jkbmscan/Stats.h +++ b/include/battery/jkbmscan/Stats.h @@ -11,7 +11,7 @@ friend class Provider; public: void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - bool getImmediateChargingRequest() const { return _chargeImmediately; } ; + // bool getImmediateChargingRequest() const { return _chargeImmediately; } ; float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; private: @@ -20,9 +20,24 @@ friend class Provider; float _chargeVoltage; float _chargeCurrentLimitation; float _dischargeVoltageLimitation; - uint16_t _stateOfHealth; + uint8_t _stateOfHealth; float _temperature; + float _cellVoltage[25]; + 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; @@ -41,7 +56,13 @@ friend class Provider; bool _chargeEnabled; bool _dischargeEnabled; - bool _chargeImmediately; + bool _balanceEnabled; + bool _heaterEnabled; + bool _accEnabled; + bool _chargerPluged; + + bool _chargeRequest; + bool _chargeAndHeat; uint8_t _moduleCount; }; diff --git a/include/defaults.h b/include/defaults.h index e341181f4..7e17dfb49 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -152,6 +152,7 @@ #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_ENABLE_DISCHARGE_CURRENT_LIMIT false #define BATTERY_DISCHARGE_CURRENT_LIMIT 0.0 #define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC 100.0 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 34e7cf640..8a9c103df 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -180,6 +180,11 @@ void ConfigurationClass::serializeBatterySerialConfig(BatterySerialConfig const& target["polling_interval"] = source.PollingInterval; } +void ConfigurationClass::serializeBatteryJkBmsCanConfig(BatteryJkBmsCanConfig const& source, JsonObject& target) +{ + target["number_of_cells"] = source.number_of_cells; +} + void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target) { char serialBuffer[sizeof(uint64_t) * 8 + 1]; @@ -435,6 +440,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); @@ -602,6 +610,11 @@ void ConfigurationClass::deserializeBatterySerialConfig(JsonObject const& source target.PollingInterval = source["polling_interval"] | BATTERY_SERIAL_POLLING_INTERVAL; } +void ConfigurationClass::deserializeBatteryJkBmsCanConfig(JsonObject const& source, BatteryJkBmsCanConfig& target) +{ + target.number_of_cells = source["number_of_cells"] | BATTERY_NUMBER_OF_CELLS; +} + void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target) { auto serialBin = [](String const& input) -> uint64_t { @@ -882,6 +895,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/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/jkbmscan/HassIntegration.cpp b/src/battery/jkbmscan/HassIntegration.cpp index 4fdefd974..0a8120384 100644 --- a/src/battery/jkbmscan/HassIntegration.cpp +++ b/src/battery/jkbmscan/HassIntegration.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include namespace Batteries::JkBmsCan { @@ -10,6 +11,8 @@ HassIntegration::HassIntegration(std::shared_ptr 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", "%"); @@ -43,6 +46,38 @@ void HassIntegration::publishSensors() const 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 index 720f59b03..2ba63c738 100644 --- a/src/battery/jkbmscan/Provider.cpp +++ b/src/battery/jkbmscan/Provider.cpp @@ -19,35 +19,10 @@ bool Provider::init(bool verboseLogging) void Provider::onMessage(twai_message_t rx_message) { switch (rx_message.identifier) { - case 0x351: { - _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); - _stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); - _stats->setDischargeCurrentLimit(this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1), millis()); - _stats->_dischargeVoltageLimitation = this->scaleValue(this->readUnsignedInt16(rx_message.data + 6), 0.1); - - if (_verboseLogging) { - MessageOutput.printf("[JkBmsCan] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f dischargeVoltageLimitation: %f\r\n", - _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit(), - _stats->_dischargeVoltageLimitation); - } - break; - } - - case 0x355: { - _stats->setSoC(static_cast(this->readUnsignedInt16(rx_message.data)), 0/*precision*/, millis()); - _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); - - if (_verboseLogging) { - MessageOutput.printf("[JkBmsCan] soc: %f soh: %d\r\n", - _stats->getSoC(), _stats->_stateOfHealth); - } - break; - } case 0x02F4: { _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.1), 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()); - // _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); String manufacturer = "JKBMS ID: 0"; if (manufacturer.isEmpty()) { break; } @@ -58,6 +33,14 @@ void Provider::onMessage(twai_message_t rx_message) _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; if (_verboseLogging) { @@ -66,23 +49,66 @@ void Provider::onMessage(twai_message_t rx_message) } break; } + - case 0x18F128F4: { + 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 0x356: { - _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); - _stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis()); - _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); - if (_verboseLogging) { - MessageOutput.printf("[JkBmsCan] voltage: %f current: %f temperature: %f\r\n", - _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); - } + 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 0x359: { + case 0x18F328F4: { uint16_t alarmBits = rx_message.data[0]; _stats->_alarmOverCurrentDischarge = this->getBit(alarmBits, 7); _stats->_alarmUnderTemperature = this->getBit(alarmBits, 4); @@ -126,34 +152,51 @@ void Provider::onMessage(twai_message_t rx_message) _stats->_warningBmsInternal, _stats->_warningHighCurrentCharge); } - - _stats->_moduleCount = rx_message.data[4]; - if (_verboseLogging) { - MessageOutput.printf("[JkBmsCan] Modules: %d\r\n", - _stats->_moduleCount); - } - 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 0x35C: { + case 0x18F528F4: { uint16_t chargeStatusBits = rx_message.data[0]; - _stats->_chargeEnabled = this->getBit(chargeStatusBits, 7); - _stats->_dischargeEnabled = this->getBit(chargeStatusBits, 6); - _stats->_chargeImmediately = this->getBit(chargeStatusBits, 5); + _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); if (_verboseLogging) { MessageOutput.printf("[JkBmsCan] chargeStatusBits: %d %d %d\r\n", _stats->_chargeEnabled, _stats->_dischargeEnabled, - _stats->_chargeImmediately); + _stats->_balanceEnabled); } break; } + case 0x1806E5F4: { + _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); + _stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); + _stats->_chargeRequest = this->readUnsignedInt8(rx_message.data + 4); + _stats->_chargeAndHeat = this->readUnsignedInt8(rx_message.data + 5); + + + + if (_verboseLogging) { + MessageOutput.printf("[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; @@ -162,85 +205,6 @@ void Provider::onMessage(twai_message_t rx_message) _stats->setLastUpdate(millis()); } -// Currently not called because there is no nice way to integrate it right now -#ifdef PYLONTECH_DUMMY -void Provider::dummyData() -{ - static uint32_t lastUpdate = millis(); - static uint8_t issues = 0; - - if (millis() < (lastUpdate + 5 * 1000)) { return; } - - lastUpdate = millis(); - _stats->setLastUpdate(lastUpdate); - - auto dummyFloat = [](int offset) -> float { - return offset + (static_cast((lastUpdate + offset) % 10) / 10); - }; - - _stats->setManufacturer("JkBmsCan US3000C"); - _stats->setSoC(42, 0/*precision*/, millis()); - _stats->_chargeVoltage = dummyFloat(50); - _stats->_chargeCurrentLimitation = dummyFloat(33); - _stats->setDischargeCurrentLimit(dummyFloat(12), millis()); - _stats->_dischargeVoltageLimitation = dummyFloat(46); - _stats->_stateOfHealth = 99; - _stats->setVoltage(48.67, millis()); - _stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis()); - _stats->_temperature = dummyFloat(20); - - _stats->_chargeEnabled = true; - _stats->_dischargeEnabled = true; - _stats->_chargeImmediately = false; - - _stats->_moduleCount = 1; - - _stats->_warningHighCurrentDischarge = false; - _stats->_warningHighCurrentCharge = false; - _stats->_warningLowTemperature = false; - _stats->_warningHighTemperature = false; - _stats->_warningLowVoltage = false; - _stats->_warningHighVoltage = false; - _stats->_warningBmsInternal = false; - - _stats->_alarmOverCurrentDischarge = false; - _stats->_alarmOverCurrentCharge = false; - _stats->_alarmUnderTemperature = false; - _stats->_alarmOverTemperature = false; - _stats->_alarmUnderVoltage = false; - _stats->_alarmOverVoltage = false; - _stats->_alarmBmsInternal = false; - - if (issues == 1 || issues == 3) { - _stats->_warningHighCurrentDischarge = true; - _stats->_warningHighCurrentCharge = true; - _stats->_warningLowTemperature = true; - _stats->_warningHighTemperature = true; - _stats->_warningLowVoltage = true; - _stats->_warningHighVoltage = true; - _stats->_warningBmsInternal = true; - } - - if (issues == 2 || issues == 3) { - _stats->_alarmOverCurrentDischarge = true; - _stats->_alarmOverCurrentCharge = true; - _stats->_alarmUnderTemperature = true; - _stats->_alarmOverTemperature = true; - _stats->_alarmUnderVoltage = true; - _stats->_alarmOverVoltage = true; - _stats->_alarmBmsInternal = true; - } - if (issues == 4) { - _stats->_warningHighCurrentCharge = true; - _stats->_warningLowTemperature = true; - _stats->_alarmUnderVoltage = true; - _stats->_dischargeEnabled = false; - _stats->_chargeImmediately = true; - } - - issues = (issues + 1) % 5; -} -#endif } // namespace Batteries::JkBmsCan diff --git a/src/battery/jkbmscan/Stats.cpp b/src/battery/jkbmscan/Stats.cpp index 17eacafc3..a36bd07b7 100644 --- a/src/battery/jkbmscan/Stats.cpp +++ b/src/battery/jkbmscan/Stats.cpp @@ -1,12 +1,17 @@ // 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); @@ -15,10 +20,42 @@ void Stats::getLiveViewData(JsonVariant& root) const 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])); + if (1) { + MessageOutput.printf("[JkBmsCan] %s: %f \r\n", + cellno.c_str(), _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 60ae8feb1..edaad7ad7 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -776,6 +776,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 682daf011..68ddb3f42 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -777,6 +777,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 7a93e8c51..19b0e9fc3 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -608,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..5187a8557 100644 --- a/webapp/src/types/BatteryConfig.ts +++ b/webapp/src/types/BatteryConfig.ts @@ -38,12 +38,17 @@ export interface BatterySerialConfig { polling_interval: number; } +export interface BatteryJkBmsCanConfig { + number_of_cells: 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 e32ac1dc7..4c7409e3f 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -69,6 +69,23 @@ /> + + + +