diff --git a/include/BatteryCanReceiver.h b/include/BatteryCanReceiver.h index cf3864a8f..d9565ee39 100644 --- a/include/BatteryCanReceiver.h +++ b/include/BatteryCanReceiver.h @@ -14,8 +14,10 @@ class BatteryCanReceiver : public BatteryProvider { virtual void onMessage(twai_message_t rx_message) = 0; protected: + uint8_t readUnsignedInt8(uint8_t *data); uint16_t readUnsignedInt16(uint8_t *data); int16_t readSignedInt16(uint8_t *data); + uint32_t readUnsignedInt32(uint8_t *data); float scaleValue(int16_t value, float factor); bool getBit(uint8_t value, uint8_t bit); diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 0f86c7c5d..c527b827c 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -60,6 +60,7 @@ class BatteryStats { String _manufacturer = "unknown"; String _hwversion = ""; String _fwversion = ""; + String _serial = ""; uint32_t _lastUpdate = 0; private: @@ -115,6 +116,84 @@ class PylontechBatteryStats : public BatteryStats { bool _chargeImmediately; }; +class PytesBatteryStats : public BatteryStats { + friend class PytesCanReceiver; + + public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + float getChargeCurrent() const { return _current; } ; + float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ; + + private: + void setManufacturer(String&& m) { _manufacturer = std::move(m); } + void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } + void updateSerial() { + if (!_serialPart1.isEmpty() && !_serialPart2.isEmpty()) { + _serial = _serialPart1 + _serialPart2; + } + } + + String _serialPart1 = ""; + String _serialPart2 = ""; + + float _chargeVoltageLimit; + float _chargeCurrentLimit; + float _dischargeVoltageLimit; + float _dischargeCurrentLimit; + + uint16_t _stateOfHealth; + + // total current into (positive) or from (negative) + // the battery, i.e., the charging current + float _current; + float _temperature; + + uint16_t _cellMinMilliVolt; + uint16_t _cellMaxMilliVolt; + float _cellMinTemperature; + float _cellMaxTemperature; + + String _cellMinVoltageName; + String _cellMaxVoltageName; + String _cellMinTemperatureName; + String _cellMaxTemperatureName; + + uint8_t _moduleCountOnline; + uint8_t _moduleCountOffline; + + uint8_t _moduleCountBlockingCharge; + uint8_t _moduleCountBlockingDischarge; + + uint16_t _totalCapacity; + uint16_t _availableCapacity; + + float _chargedEnergy = -1; + float _dischargedEnergy = -1; + + bool _alarmUnderVoltage; + bool _alarmOverVoltage; + bool _alarmOverCurrentCharge; + bool _alarmOverCurrentDischarge; + bool _alarmUnderTemperature; + bool _alarmOverTemperature; + bool _alarmUnderTemperatureCharge; + bool _alarmOverTemperatureCharge; + bool _alarmInternalFailure; + bool _alarmCellImbalance; + + bool _warningLowVoltage; + bool _warningHighVoltage; + bool _warningHighChargeCurrent; + bool _warningHighDischargeCurrent; + bool _warningLowTemperature; + bool _warningHighTemperature; + bool _warningLowTemperatureCharge; + bool _warningHighTemperatureCharge; + bool _warningInternalFailure; + bool _warningCellImbalance; +}; + class JkBmsBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final { diff --git a/include/PytesCanReceiver.h b/include/PytesCanReceiver.h new file mode 100644 index 000000000..aaeb9d66d --- /dev/null +++ b/include/PytesCanReceiver.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include "Battery.h" +#include "BatteryCanReceiver.h" +#include + +class PytesCanReceiver : public BatteryCanReceiver { +public: + bool init(bool verboseLogging) final; + void onMessage(twai_message_t rx_message) final; + + std::shared_ptr getStats() const final { return _stats; } + +private: + std::shared_ptr _stats = + std::make_shared(); +}; diff --git a/src/Battery.cpp b/src/Battery.cpp index 794afa51c..79ba70020 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,6 +5,7 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" +#include "PytesCanReceiver.h" BatteryClass Battery; @@ -57,6 +58,9 @@ void BatteryClass::updateSettings() case 3: _upProvider = std::make_unique(); break; + case 4: + _upProvider = std::make_unique(); + break; default: MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider); return; diff --git a/src/BatteryCanReceiver.cpp b/src/BatteryCanReceiver.cpp index c780b9272..aca563bbb 100644 --- a/src/BatteryCanReceiver.cpp +++ b/src/BatteryCanReceiver.cpp @@ -130,15 +130,28 @@ void BatteryCanReceiver::loop() return; } + if (_verboseLogging) { + MessageOutput.printf("[%s] Received CAN message: 0x%04X -", + _providerName, rx_message.identifier); + + for (int i = 0; i < rx_message.data_length_code; i++) { + MessageOutput.printf(" %02X", rx_message.data[i]); + } + + MessageOutput.printf("\r\n"); + } + onMessage(rx_message); } +uint8_t BatteryCanReceiver::readUnsignedInt8(uint8_t *data) +{ + return data[0]; +} + uint16_t BatteryCanReceiver::readUnsignedInt16(uint8_t *data) { - uint8_t bytes[2]; - bytes[0] = *data; - bytes[1] = *(data + 1); - return (bytes[1] << 8) + bytes[0]; + return (data[1] << 8) | data[0]; } int16_t BatteryCanReceiver::readSignedInt16(uint8_t *data) @@ -146,6 +159,11 @@ int16_t BatteryCanReceiver::readSignedInt16(uint8_t *data) return this->readUnsignedInt16(data); } +uint32_t BatteryCanReceiver::readUnsignedInt32(uint8_t *data) +{ + return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0]; +} + float BatteryCanReceiver::scaleValue(int16_t value, float factor) { return value * factor; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 61f23823f..34542b66f 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -26,9 +26,12 @@ static void addLiveViewValue(JsonVariant& root, std::string const& name, } static void addLiveViewTextInSection(JsonVariant& root, - std::string const& section, std::string const& name, std::string const& text) + std::string const& section, std::string const& name, + std::string const& text, bool translate = true) { - root["values"][section][name] = text; + auto jsonValue = root["values"][section][name]; + jsonValue["value"] = text; + jsonValue["translate"] = translate; } static void addLiveViewTextValue(JsonVariant& root, std::string const& name, @@ -62,6 +65,9 @@ bool BatteryStats::updateAvailable(uint32_t since) const void BatteryStats::getLiveViewData(JsonVariant& root) const { root["manufacturer"] = _manufacturer; + if (!_serial.isEmpty()) { + root["serial"] = _serial; + } if (!_fwversion.isEmpty()) { root["fwversion"] = _fwversion; } @@ -113,6 +119,78 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal); } +void PytesBatteryStats::getLiveViewData(JsonVariant& root) const +{ + BatteryStats::getLiveViewData(root); + + // values go into the "Status" card of the web application + addLiveViewValue(root, "current", _current, "A", 1); + addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1); + addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1); + addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1); + addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1); + addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); + addLiveViewValue(root, "temperature", _temperature, "°C", 1); + + addLiveViewValue(root, "capacity", _totalCapacity, "Ah", 0); + addLiveViewValue(root, "availableCapacity", _availableCapacity, "Ah", 0); + + if (_chargedEnergy != -1) { + addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2); + } + + if (_dischargedEnergy != -1) { + addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2); + } + + addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast(_cellMinMilliVolt)/1000, "V", 3); + addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast(_cellMaxMilliVolt)/1000, "V", 3); + addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0); + addLiveViewInSection(root, "cells", "cellMinTemperature", _cellMinTemperature, "°C", 0); + addLiveViewInSection(root, "cells", "cellMaxTemperature", _cellMaxTemperature, "°C", 0); + + addLiveViewTextInSection(root, "cells", "cellMinVoltageName", _cellMinVoltageName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMaxVoltageName", _cellMaxVoltageName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMinTemperatureName", _cellMinTemperatureName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMaxTemperatureName", _cellMaxTemperatureName.c_str(), false); + + addLiveViewInSection(root, "modules", "online", _moduleCountOnline, "", 0); + addLiveViewInSection(root, "modules", "offline", _moduleCountOffline, "", 0); + addLiveViewInSection(root, "modules", "blockingCharge", _moduleCountBlockingCharge, "", 0); + addLiveViewInSection(root, "modules", "blockingDischarge", _moduleCountBlockingDischarge, "", 0); + + // alarms and warnings go into the "Issues" card of the web application + addLiveViewWarning(root, "highCurrentDischarge", _warningHighDischargeCurrent); + addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge); + + addLiveViewWarning(root, "highCurrentCharge", _warningHighChargeCurrent); + addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge); + + addLiveViewWarning(root, "lowVoltage", _warningLowVoltage); + addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); + + addLiveViewWarning(root, "highVoltage", _warningHighVoltage); + addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); + + addLiveViewWarning(root, "lowTemperature", _warningLowTemperature); + addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); + + addLiveViewWarning(root, "highTemperature", _warningHighTemperature); + addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); + + addLiveViewWarning(root, "lowTemperatureCharge", _warningLowTemperatureCharge); + addLiveViewAlarm(root, "underTemperatureCharge", _alarmUnderTemperatureCharge); + + addLiveViewWarning(root, "highTemperatureCharge", _warningHighTemperatureCharge); + addLiveViewAlarm(root, "overTemperatureCharge", _alarmOverTemperatureCharge); + + addLiveViewWarning(root, "bmsInternal", _warningInternalFailure); + addLiveViewAlarm(root, "bmsInternal", _alarmInternalFailure); + + addLiveViewWarning(root, "cellDiffVoltage", _warningCellImbalance); + addLiveViewAlarm(root, "cellDiffVoltage", _alarmCellImbalance); +} + void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const { BatteryStats::getLiveViewData(root); @@ -259,6 +337,68 @@ void PylontechBatteryStats::mqttPublish() const MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); } +void PytesBatteryStats::mqttPublish() const +{ + BatteryStats::mqttPublish(); + + MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltageLimit)); + MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimit)); + MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit)); + MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit)); + + MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); + MqttSettings.publish("battery/current", String(_current)); + MqttSettings.publish("battery/temperature", String(_temperature)); + + if (_chargedEnergy != -1) { + MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); + } + + if (_dischargedEnergy != -1) { + MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); + } + + MqttSettings.publish("battery/capacity", String(_totalCapacity)); + MqttSettings.publish("battery/availableCapacity", String(_availableCapacity)); + + MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt)); + MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt)); + MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt)); + MqttSettings.publish("battery/CellMinTemperature", String(_cellMinTemperature)); + MqttSettings.publish("battery/CellMaxTemperature", String(_cellMaxTemperature)); + MqttSettings.publish("battery/CellMinVoltageName", String(_cellMinVoltageName)); + MqttSettings.publish("battery/CellMaxVoltageName", String(_cellMaxVoltageName)); + MqttSettings.publish("battery/CellMinTemperatureName", String(_cellMinTemperatureName)); + MqttSettings.publish("battery/CellMaxTemperatureName", String(_cellMaxTemperatureName)); + + MqttSettings.publish("battery/modulesOnline", String(_moduleCountOnline)); + MqttSettings.publish("battery/modulesOffline", String(_moduleCountOffline)); + MqttSettings.publish("battery/modulesBlockingCharge", String(_moduleCountBlockingCharge)); + MqttSettings.publish("battery/modulesBlockingDischarge", String(_moduleCountBlockingDischarge)); + + MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); + MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); + MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); + MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); + MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); + MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); + MqttSettings.publish("battery/alarm/underTemperatureCharge", String(_alarmUnderTemperatureCharge)); + MqttSettings.publish("battery/alarm/overTemperatureCharge", String(_alarmOverTemperatureCharge)); + MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmInternalFailure)); + MqttSettings.publish("battery/alarm/cellImbalance", String(_alarmCellImbalance)); + + MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighDischargeCurrent)); + MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighChargeCurrent)); + MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); + MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); + MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); + MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); + MqttSettings.publish("battery/warning/lowTemperatureCharge", String(_warningLowTemperatureCharge)); + MqttSettings.publish("battery/warning/highTemperatureCharge", String(_warningHighTemperatureCharge)); + MqttSettings.publish("battery/warning/bmsInternal", String(_warningInternalFailure)); + MqttSettings.publish("battery/warning/cellImbalance", String(_warningCellImbalance)); +} + void JkBmsBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index c91b855af..5ceb2c6ab 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -125,6 +125,61 @@ void MqttHandleBatteryHassClass::loop() publishSensor("Midpoint Voltage", NULL, "midpointVoltage", "voltage", "measurement", "V"); publishSensor("Midpoint Deviation", NULL, "midpointDeviation", "battery", "measurement", "%"); break; + case 4: // Pytes Battery + publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V"); + publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge voltage limit", NULL, "settings/dischargeVoltageLimitation", "voltage", "measurement", "V"); + + publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V"); + publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A"); + publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%"); + publishSensor("Temperature", "mdi:thermometer", "temperature", "temperature", "measurement", "°C"); + + publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh"); + publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh"); + + publishSensor("Total Capacity", NULL, "capacity"); + publishSensor("Available Capacity", NULL, "availableCapacity"); + + publishSensor("Cell Min Voltage", NULL, "CellMinMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Max Voltage", NULL, "CellMaxMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Min Temperature", NULL, "CellMinTemperature", "temperature", "measurement", "°C"); + publishSensor("Cell Max Temperature", NULL, "CellMaxTemperature", "temperature", "measurement", "°C"); + + publishSensor("Cell Min Voltage Label", NULL, "CellMinVoltageName"); + publishSensor("Cell Max Voltage Label", NULL, "CellMaxVoltageName"); + publishSensor("Cell Min Temperature Label", NULL, "CellMinTemperatureName"); + publishSensor("Cell Max Temperature Label", NULL, "CellMaxTemperatureName"); + + publishSensor("Modules Online", "mdi:counter", "modulesOnline"); + publishSensor("Modules Offline", "mdi:counter", "modulesOffline"); + publishSensor("Modules Blocking Charge", "mdi:counter", "modulesBlockingCharge"); + publishSensor("Modules Blocking Discharge", "mdi:counter", "modulesBlockingDischarge"); + + publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0"); + publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0"); + publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0"); + publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0"); + publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0"); + publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0"); + publishBinarySensor("Alarm Temperature low (charge)", "mdi:thermometer-low", "alarm/underTemperatureCharge", "1", "0"); + publishBinarySensor("Alarm Temperature high (charge)", "mdi:thermometer-high", "alarm/overTemperatureCharge", "1", "0"); + publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0"); + publishBinarySensor("Alarm Cell Imbalance", "mdi:alert-outline", "alarm/cellImbalance", "1", "0"); + + publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0"); + publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0"); + publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0"); + publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0"); + publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0"); + publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0"); + publishBinarySensor("Warning Temperature low (charge)", "mdi:thermometer-low", "warning/lowTemperatureCharge", "1", "0"); + publishBinarySensor("Warning Temperature high (charge)", "mdi:thermometer-high", "warning/highTemperatureCharge", "1", "0"); + publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0"); + publishBinarySensor("Warning Cell Imbalance", "mdi:alert-outline", "warning/cellImbalance", "1", "0"); + break; } _doPublish = false; diff --git a/src/PytesCanReceiver.cpp b/src/PytesCanReceiver.cpp new file mode 100644 index 000000000..805d59fab --- /dev/null +++ b/src/PytesCanReceiver.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PytesCanReceiver.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include +#include + +bool PytesCanReceiver::init(bool verboseLogging) +{ + return BatteryCanReceiver::init(verboseLogging, "Pytes"); +} + +void PytesCanReceiver::onMessage(twai_message_t rx_message) +{ + switch (rx_message.identifier) { + case 0x351: { + _stats->_chargeVoltageLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); + _stats->_chargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 2), 0.1); + _stats->_dischargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 4), 0.1); + _stats->_dischargeVoltageLimit = this->scaleValue(this->readSignedInt16(rx_message.data + 6), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] chargeVoltageLimit: %f chargeCurrentLimit: %f dischargeCurrentLimit: %f dischargeVoltageLimit: %f\r\n", + _stats->_chargeVoltageLimit, _stats->_chargeCurrentLimit, + _stats->_dischargeCurrentLimit, _stats->_dischargeVoltageLimit); + } + 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("[Pytes] soc: %d soh: %d\r\n", + _stats->getSoC(), _stats->_stateOfHealth); + } + break; + } + + case 0x356: { + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); + _stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); + _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] voltage: %f current: %f temperature: %f\r\n", + _stats->getVoltage(), _stats->_current, _stats->_temperature); + } + break; + } + + case 0x35A: { // Alarms and Warnings + uint16_t alarmBits = rx_message.data[0]; + _stats->_alarmOverVoltage = this->getBit(alarmBits, 2); + _stats->_alarmUnderVoltage = this->getBit(alarmBits, 4); + _stats->_alarmOverTemperature = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[1]; + _stats->_alarmUnderTemperature = this->getBit(alarmBits, 0); + _stats->_alarmOverTemperatureCharge = this->getBit(alarmBits, 2); + _stats->_alarmUnderTemperatureCharge = this->getBit(alarmBits, 4); + _stats->_alarmOverCurrentDischarge = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[2]; + _stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0); + _stats->_alarmInternalFailure = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[3]; + _stats->_alarmCellImbalance = this->getBit(alarmBits, 0); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Alarms: %d %d %d %d %d %d %d %d %d %d\r\n", + _stats->_alarmOverVoltage, + _stats->_alarmUnderVoltage, + _stats->_alarmOverTemperature, + _stats->_alarmUnderTemperature, + _stats->_alarmOverTemperatureCharge, + _stats->_alarmUnderTemperatureCharge, + _stats->_alarmOverCurrentDischarge, + _stats->_alarmOverCurrentCharge, + _stats->_alarmInternalFailure, + _stats->_alarmCellImbalance); + } + + uint16_t warningBits = rx_message.data[4]; + _stats->_warningHighVoltage = this->getBit(warningBits, 2); + _stats->_warningLowVoltage = this->getBit(warningBits, 4); + _stats->_warningHighTemperature = this->getBit(warningBits, 6); + + warningBits = rx_message.data[5]; + _stats->_warningLowTemperature = this->getBit(warningBits, 0); + _stats->_warningHighTemperatureCharge = this->getBit(warningBits, 2); + _stats->_warningLowTemperatureCharge = this->getBit(warningBits, 4); + _stats->_warningHighDischargeCurrent = this->getBit(warningBits, 6); + + warningBits = rx_message.data[6]; + _stats->_warningHighChargeCurrent = this->getBit(warningBits, 0); + _stats->_warningInternalFailure = this->getBit(warningBits, 6); + + warningBits = rx_message.data[7]; + _stats->_warningCellImbalance = this->getBit(warningBits, 0); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Warnings: %d %d %d %d %d %d %d %d %d %d\r\n", + _stats->_warningHighVoltage, + _stats->_warningLowVoltage, + _stats->_warningHighTemperature, + _stats->_warningLowTemperature, + _stats->_warningHighTemperatureCharge, + _stats->_warningLowTemperatureCharge, + _stats->_warningHighDischargeCurrent, + _stats->_warningHighChargeCurrent, + _stats->_warningInternalFailure, + _stats->_warningCellImbalance); + } + break; + } + + case 0x35E: { + String manufacturer(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (manufacturer.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Manufacturer: %s\r\n", manufacturer.c_str()); + } + + _stats->setManufacturer(std::move(manufacturer)); + break; + } + + case 0x35F: { // BatteryInfo + auto fwVersionPart1 = String(this->readUnsignedInt8(rx_message.data + 2)); + auto fwVersionPart2 = String(this->readUnsignedInt8(rx_message.data + 3)); + _stats->_fwversion = "v" + fwVersionPart1 + "." + fwVersionPart2; + + _stats->_availableCapacity = this->readUnsignedInt16(rx_message.data + 4); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] fwversion: %s availableCapacity: %d Ah\r\n", + _stats->_fwversion.c_str(), _stats->_availableCapacity); + } + break; + } + + case 0x372: { // BankInfo + _stats->_moduleCountOnline = this->readUnsignedInt16(rx_message.data); + _stats->_moduleCountBlockingCharge = this->readUnsignedInt16(rx_message.data + 2); + _stats->_moduleCountBlockingDischarge = this->readUnsignedInt16(rx_message.data + 4); + _stats->_moduleCountOffline = this->readUnsignedInt16(rx_message.data + 6); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] moduleCountOnline: %d moduleCountBlockingCharge: %d moduleCountBlockingDischarge: %d moduleCountOffline: %d\r\n", + _stats->_moduleCountOnline, _stats->_moduleCountBlockingCharge, + _stats->_moduleCountBlockingDischarge, _stats->_moduleCountOffline); + } + break; + } + + case 0x373: { // CellInfo + _stats->_cellMinMilliVolt = this->readUnsignedInt16(rx_message.data); + _stats->_cellMaxMilliVolt = this->readUnsignedInt16(rx_message.data + 2); + _stats->_cellMinTemperature = this->readUnsignedInt16(rx_message.data + 4) - 273; + _stats->_cellMaxTemperature = this->readUnsignedInt16(rx_message.data + 6) - 273; + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] lowestCellMilliVolt: %d highestCellMilliVolt: %d minimumCellTemperature: %f maximumCellTemperature: %f\r\n", + _stats->_cellMinMilliVolt, _stats->_cellMaxMilliVolt, + _stats->_cellMinTemperature, _stats->_cellMaxTemperature); + } + break; + } + + case 0x374: { // Battery/Cell name (string) with "Lowest Cell Voltage" + String cellMinVoltageName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMinVoltageName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMinVoltageName: %s\r\n", + cellMinVoltageName.c_str()); + } + + _stats->_cellMinVoltageName = cellMinVoltageName; + break; + } + + case 0x375: { // Battery/Cell name (string) with "Highest Cell Voltage" + String cellMaxVoltageName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMaxVoltageName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMaxVoltageName: %s\r\n", + cellMaxVoltageName.c_str()); + } + + _stats->_cellMaxVoltageName = cellMaxVoltageName; + break; + } + + case 0x376: { // Battery/Cell name (string) with "Minimum Cell Temperature" + String cellMinTemperatureName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMinTemperatureName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMinTemperatureName: %s\r\n", + cellMinTemperatureName.c_str()); + } + + _stats->_cellMinTemperatureName = cellMinTemperatureName; + break; + } + + case 0x377: { // Battery/Cell name (string) with "Maximum Cell Temperature" + String cellMaxTemperatureName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMaxTemperatureName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMaxTemperatureName: %s\r\n", + cellMaxTemperatureName.c_str()); + } + + _stats->_cellMaxTemperatureName = cellMaxTemperatureName; + break; + } + + case 0x378: { // History: Charged / Discharged Energy + _stats->_chargedEnergy = this->scaleValue(this->readUnsignedInt32(rx_message.data), 0.1); + _stats->_dischargedEnergy = this->scaleValue(this->readUnsignedInt32(rx_message.data + 4), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] chargedEnergy: %f dischargedEnergy: %f\r\n", + _stats->_chargedEnergy, _stats->_dischargedEnergy); + } + break; + } + + case 0x379: { // BatterySize: Installed Ah + _stats->_totalCapacity = this->readUnsignedInt16(rx_message.data); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] totalCapacity: %d Ah\r\n", + _stats->_totalCapacity); + } + break; + } + + case 0x380: { // Serialnumber - part 1 + String snPart1(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (snPart1.isEmpty() || !isgraph(snPart1.charAt(0))) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] snPart1: %s\r\n", snPart1.c_str()); + } + + _stats->_serialPart1 = snPart1; + _stats->updateSerial(); + break; + } + + case 0x381: { // Serialnumber - part 2 + String snPart2(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (snPart2.isEmpty() || !isgraph(snPart2.charAt(0))) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] snPart2: %s\r\n", snPart2.c_str()); + } + + _stats->_serialPart2 = snPart2; + _stats->updateSerial(); + break; + } + + default: + return; // do not update last update timestamp + break; + } + + _stats->setLastUpdate(millis()); +} diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue index b381859a1..52c7b36fe 100644 --- a/webapp/src/components/BatteryView.vue +++ b/webapp/src/components/BatteryView.vue @@ -18,6 +18,9 @@
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
+
+ {{ $t('home.SerialNumber') }}{{ batteryData.serial }} +
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
@@ -49,18 +52,24 @@ {{ $t('battery.' + key) }} -