Feature: add support for Pytes batteries using CAN (#1088)

Co-authored-by: Bernhard Kirchen <schlimmchen@posteo.net>
This commit is contained in:
Andreas Böhm 2024-07-10 21:01:49 +02:00 committed by GitHub
parent 83c59d7811
commit 6a3f90ff95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 684 additions and 16 deletions

View File

@ -14,8 +14,10 @@ public:
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);

View File

@ -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 {

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include "Battery.h"
#include "BatteryCanReceiver.h"
#include <driver/twai.h>
class PytesCanReceiver : public BatteryCanReceiver {
public:
bool init(bool verboseLogging) final;
void onMessage(twai_message_t rx_message) final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
std::shared_ptr<PytesBatteryStats> _stats =
std::make_shared<PytesBatteryStats>();
};

View File

@ -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<VictronSmartShunt>();
break;
case 4:
_upProvider = std::make_unique<PytesCanReceiver>();
break;
default:
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
return;

View File

@ -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;

View File

@ -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<float>(_cellMinMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_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();

View File

@ -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;

293
src/PytesCanReceiver.cpp Normal file
View File

@ -0,0 +1,293 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "PytesCanReceiver.h"
#include "MessageOutput.h"
#include "PinMapping.h"
#include <driver/twai.h>
#include <ctime>
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<uint8_t>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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());
}

View File

@ -18,6 +18,9 @@
<div style="padding-right: 2em;">
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
</div>
<div style="padding-right: 2em;" v-if="'serial' in batteryData">
{{ $t('home.SerialNumber') }}{{ batteryData.serial }}
</div>
<div style="padding-right: 2em;" v-if="'fwversion' in batteryData">
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
</div>
@ -49,18 +52,24 @@
<tr v-for="(prop, key) in values" v-bind:key="key">
<th scope="row">{{ $t('battery.' + key) }}</th>
<td style="text-align: right">
<template v-if="typeof prop === 'string'">
{{ $t('battery.' + prop) }}
<template v-if="isStringValue(prop) && prop.translate">
{{ $t('battery.' + prop.value) }}
</template>
<template v-else-if="isStringValue(prop)">
{{ prop.value }}
</template>
<template v-else>
{{ $n(prop.v, 'decimal', {
minimumFractionDigits: prop.d,
maximumFractionDigits: prop.d})
}}
{{ $n(prop.v, 'decimal', {
minimumFractionDigits: prop.d,
maximumFractionDigits: prop.d})
}}
</template>
</td>
<td>
<template v-if="!isStringValue(prop)">
{{prop.u}}
</template>
</td>
<td v-if="typeof prop === 'string'"></td>
<td v-else>{{prop.u}}</td>
</tr>
</tbody>
</table>
@ -114,7 +123,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import type { Battery } from '@/types/BatteryDataStatus';
import type { Battery, StringValue } from '@/types/BatteryDataStatus';
import type { ValueObject } from '@/types/LiveDataStatus';
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
export default defineComponent({
@ -144,6 +154,9 @@ export default defineComponent({
this.closeSocket();
},
methods: {
isStringValue(value: ValueObject | StringValue) : value is StringValue {
return value && typeof value === 'object' && 'translate' in value;
},
getInitialData() {
console.log("Get initalData for Battery");
this.dataLoading = true;

View File

@ -654,6 +654,7 @@
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"ProviderPytesCan": "Pytes per CAN-Bus",
"MqttConfiguration": "MQTT Einstellungen",
"MqttSocTopic": "Topic für Batterie-SoC",
"MqttVoltageTopic": "Topic für Batteriespannung",
@ -891,14 +892,22 @@
"voltage": "Spannung",
"current": "Strom",
"power": "Leistung",
"capacity": "Gesamtkapazität",
"availableCapacity": "Verfügbare Kapazität",
"temperature": "Temperatur",
"bmsTemp": "BMS-Temperatur",
"chargeVoltage": "Gewünschte Ladespannung (BMS)",
"chargeCurrentLimitation": "Ladestromlimit",
"dischargeCurrentLimitation": "Entladestromlimit",
"dischargeVoltageLimitation": "Entladespannungslimit",
"chargeEnabled": "Laden ermöglicht",
"dischargeEnabled": "Entladen ermöglicht",
"chargeImmediately": "Sofortiges Laden angefordert",
"modules": "Batteriemodule",
"online": "Online",
"offline": "Offline",
"blockingCharge": "Ladung blockiert",
"blockingDischarge": "Entladung blockiert",
"cells": "Zellen",
"batOneTemp": "Batterietemperatur 1",
"batTwoTemp": "Batterietemperatur 2",
@ -906,6 +915,12 @@
"cellAvgVoltage": "Durchschnittliche Zellspannung",
"cellMaxVoltage": "Höchste Zellspannung",
"cellDiffVoltage": "Zellspannungsdifferenz",
"cellMinTemperature": "Niedrigste Zelltemperatur",
"cellMaxTemperature": "Höchste Zelltemperatur",
"cellMinVoltageName": "Kleinste Zellspannung (Label)",
"cellMaxVoltageName": "Höchste Zellspannung (Label)",
"cellMinTemperatureName": "Niedrigste Zelltemperatur (Label)",
"cellMaxTemperatureName": "Höchste Zelltemperatur (Label)",
"balancingActive": "Ausgleichen aktiv",
"issues": "Meldungen",
"noIssues": "Keine Meldungen",
@ -933,8 +948,12 @@
"overCurrentCharge": "Überstrom (Laden)",
"lowTemperature": "Geringe Temperatur",
"underTemperature": "Untertemperatur",
"lowTemperatureCharge": "Geringe Temperatur (Laden)",
"underTemperatureCharge": "Untertemperatur (Laden)",
"highTemperature": "Hohe Temperatur",
"overTemperature": "Übertemperatur",
"highTemperatureCharge": "Hohe Temperatur (Laden)",
"overTemperatureCharge": "Übertemperatur (Laden)",
"lowSOC": "Geringer Ladezustand",
"lowVoltage": "Niedrige Spannung",
"underVoltage": "Unterspannung",

View File

@ -660,6 +660,7 @@
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"ProviderPytesCan": "Pytes using CAN bus",
"MqttConfiguration": "MQTT Settings",
"MqttSocTopic": "SoC value topic",
"MqttVoltageTopic": "Voltage value topic",
@ -898,14 +899,22 @@
"voltage": "Voltage",
"current": "Current",
"power": "Power",
"capacity": "Total capacity",
"availableCapacity": "Available capacity",
"temperature": "Temperature",
"bmsTemp": "BMS temperature",
"chargeVoltage": "Requested charge voltage",
"chargeCurrentLimitation": "Charge current limit",
"dischargeCurrentLimitation": "Discharge current limit",
"dischargeVoltageLimitation": "Discharge voltage limit",
"chargeEnabled": "Charging possible",
"dischargeEnabled": "Discharging possible",
"chargeImmediately": "Immediate charging requested",
"modules": "Battery modules",
"online": "Online",
"offline": "Offline",
"blockingCharge": "Charging blocked",
"blockingDischarge": "Discharging blocked",
"cells": "Cells",
"batOneTemp": "Battery temperature 1",
"batTwoTemp": "Battery temperature 2",
@ -913,6 +922,12 @@
"cellAvgVoltage": "Average cell voltage",
"cellMaxVoltage": "Maximum cell voltage",
"cellDiffVoltage": "Cell voltage difference",
"cellMinTemperature": "Minimum cell temperature",
"cellMaxTemperature": "Maximum cell temperature",
"cellMinVoltageName": "Minimum cell voltage (label)",
"cellMaxVoltageName": "Maximum cell voltage (label)",
"cellMinTemperatureName": "Minimum cell temperature (label)",
"cellMaxTemperatureName": "Maximum cell temperature (label)",
"balancingActive": "Balancing active",
"issues": "Issues",
"noIssues": "No Issues",
@ -940,8 +955,12 @@
"overCurrentCharge": "Overcurrent (charge)",
"lowTemperature": "Low temperature",
"underTemperature": "Undertemperature",
"lowTemperatureCharge": "Low temperature (charge)",
"underTemperatureCharge": "Undertemperature (charge)",
"highTemperature": "High temperature",
"overTemperature": "Overtemperature",
"highTemperatureCharge": "High temperature (charge)",
"overTemperatureCharge": "Overtemperature (charge)",
"lowVoltage": "Low voltage",
"lowSOC": "Low state of charge",
"underVoltage": "Undervoltage",

View File

@ -1,9 +1,15 @@
import type { ValueObject } from '@/types/LiveDataStatus';
type BatteryData = (ValueObject | string)[];
export interface StringValue {
value: string;
translate: boolean;
}
type BatteryData = (ValueObject | StringValue)[];
export interface Battery {
manufacturer: string;
serial: string;
fwversion: string;
hwversion: string;
data_age: number;

View File

@ -108,6 +108,7 @@ export default defineComponent({
{ key: 1, value: 'JkBmsSerial' },
{ key: 2, value: 'Mqtt' },
{ key: 3, value: 'Victron' },
{ key: 4, value: 'PytesCan' },
],
jkBmsInterfaceTypeList: [
{ key: 0, value: 'Uart' },