Skip to content

Commit

Permalink
Feature: add support for Pytes batteries using CAN (#1088)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernhard Kirchen <schlimmchen@posteo.net>
  • Loading branch information
AndreasBoehm and schlimmchen authored Jul 10, 2024
1 parent 83c59d7 commit 6a3f90f
Show file tree
Hide file tree
Showing 13 changed files with 684 additions and 16 deletions.
2 changes: 2 additions & 0 deletions include/BatteryCanReceiver.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
79 changes: 79 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class BatteryStats {
String _manufacturer = "unknown";
String _hwversion = "";
String _fwversion = "";
String _serial = "";
uint32_t _lastUpdate = 0;

private:
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions include/PytesCanReceiver.h
Original file line number Diff line number Diff line change
@@ -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>();
};
4 changes: 4 additions & 0 deletions src/Battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "PytesCanReceiver.h"

BatteryClass Battery;

Expand Down Expand Up @@ -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;
Expand Down
26 changes: 22 additions & 4 deletions src/BatteryCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,40 @@ 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)
{
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;
Expand Down
144 changes: 142 additions & 2 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6a3f90f

Please sign in to comment.