Support for Jikong JK BMS using serial connection (#319)
This commit is contained in:
parent
2ba7ea2744
commit
f744629b0b
@ -2,44 +2,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "BatteryStats.h"
|
||||
|
||||
class BatteryProvider {
|
||||
public:
|
||||
// returns true if the provider is ready for use, false otherwise
|
||||
virtual bool init(bool verboseLogging) = 0;
|
||||
|
||||
virtual void deinit() = 0;
|
||||
virtual void loop() = 0;
|
||||
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
|
||||
};
|
||||
|
||||
class BatteryClass {
|
||||
public:
|
||||
uint32_t lastUpdate;
|
||||
void init();
|
||||
void loop();
|
||||
|
||||
float chargeVoltage;
|
||||
float chargeCurrentLimitation;
|
||||
float dischargeCurrentLimitation;
|
||||
uint16_t stateOfCharge;
|
||||
uint32_t stateOfChargeLastUpdate;
|
||||
uint16_t stateOfHealth;
|
||||
float voltage;
|
||||
float current;
|
||||
float temperature;
|
||||
bool alarmOverCurrentDischarge;
|
||||
bool alarmUnderTemperature;
|
||||
bool alarmOverTemperature;
|
||||
bool alarmUnderVoltage;
|
||||
bool alarmOverVoltage;
|
||||
|
||||
bool alarmBmsInternal;
|
||||
bool alarmOverCurrentCharge;
|
||||
|
||||
|
||||
bool warningHighCurrentDischarge;
|
||||
bool warningLowTemperature;
|
||||
bool warningHighTemperature;
|
||||
bool warningLowVoltage;
|
||||
bool warningHighVoltage;
|
||||
|
||||
bool warningBmsInternal;
|
||||
bool warningHighCurrentCharge;
|
||||
char manufacturer[9];
|
||||
bool chargeEnabled;
|
||||
bool dischargeEnabled;
|
||||
bool chargeImmediately;
|
||||
std::shared_ptr<BatteryStats const> getStats() const;
|
||||
|
||||
private:
|
||||
uint32_t _lastMqttPublish = 0;
|
||||
mutable std::mutex _mutex;
|
||||
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
|
||||
};
|
||||
|
||||
extern BatteryClass Battery;
|
||||
|
||||
98
include/BatteryStats.h
Normal file
98
include/BatteryStats.h
Normal file
@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "AsyncJson.h"
|
||||
#include "Arduino.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
// mandatory interface for all kinds of batteries
|
||||
class BatteryStats {
|
||||
public:
|
||||
String const& getManufacturer() const { return _manufacturer; }
|
||||
|
||||
// the last time *any* datum was updated
|
||||
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
|
||||
bool updateAvailable(uint32_t since) const { return _lastUpdate > since; }
|
||||
|
||||
uint8_t getSoC() const { return _SoC; }
|
||||
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
|
||||
|
||||
// convert stats to JSON for web application live view
|
||||
virtual void getLiveViewData(JsonVariant& root) const;
|
||||
|
||||
virtual void mqttPublish() const;
|
||||
|
||||
bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
|
||||
|
||||
protected:
|
||||
template<typename T>
|
||||
void addLiveViewValue(JsonVariant& root, std::string const& name,
|
||||
T&& value, std::string const& unit, uint8_t precision) const;
|
||||
void addLiveViewText(JsonVariant& root, std::string const& name,
|
||||
std::string const& text) const;
|
||||
void addLiveViewWarning(JsonVariant& root, std::string const& name,
|
||||
bool warning) const;
|
||||
void addLiveViewAlarm(JsonVariant& root, std::string const& name,
|
||||
bool alarm) const;
|
||||
|
||||
String _manufacturer = "unknown";
|
||||
uint8_t _SoC = 0;
|
||||
uint32_t _lastUpdateSoC = 0;
|
||||
uint32_t _lastUpdate = 0;
|
||||
};
|
||||
|
||||
class PylontechBatteryStats : public BatteryStats {
|
||||
friend class PylontechCanReceiver;
|
||||
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
|
||||
private:
|
||||
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
|
||||
void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = millis(); }
|
||||
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }
|
||||
|
||||
float _chargeVoltage;
|
||||
float _chargeCurrentLimitation;
|
||||
float _dischargeCurrentLimitation;
|
||||
uint16_t _stateOfHealth;
|
||||
float _voltage; // total voltage of the battery pack
|
||||
// total current into (positive) or from (negative)
|
||||
// the battery, i.e., the charging current
|
||||
float _current;
|
||||
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;
|
||||
};
|
||||
|
||||
class JkBmsBatteryStats : public BatteryStats {
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
|
||||
void updateFrom(JkBms::DataPointContainer const& dp);
|
||||
|
||||
private:
|
||||
JkBms::DataPointContainer _dataPoints;
|
||||
};
|
||||
@ -166,6 +166,11 @@ struct CONFIG_T {
|
||||
float PowerLimiter_FullSolarPassThroughStopVoltage;
|
||||
|
||||
bool Battery_Enabled;
|
||||
bool Battery_VerboseLogging;
|
||||
uint8_t Battery_Provider;
|
||||
uint8_t Battery_JkBmsInterface;
|
||||
uint8_t Battery_JkBmsPollingInterval;
|
||||
|
||||
bool Huawei_Enabled;
|
||||
bool Huawei_Auto_Power_Enabled;
|
||||
float Huawei_Auto_Power_Voltage_Limit;
|
||||
|
||||
75
include/JkBmsController.h
Normal file
75
include/JkBmsController.h
Normal file
@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "JkBmsSerialMessage.h"
|
||||
|
||||
class DataPointContainer;
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
class Controller : public BatteryProvider {
|
||||
public:
|
||||
Controller() = default;
|
||||
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
enum class Status : unsigned {
|
||||
Initializing,
|
||||
Timeout,
|
||||
WaitingForPollInterval,
|
||||
HwSerialNotAvailableForWrite,
|
||||
BusyReading,
|
||||
RequestSent,
|
||||
FrameCompleted
|
||||
};
|
||||
|
||||
std::string const& getStatusText(Status status);
|
||||
void announceStatus(Status status);
|
||||
void sendRequest(uint8_t pollInterval);
|
||||
void rxData(uint8_t inbyte);
|
||||
void reset();
|
||||
void frameComplete();
|
||||
void processDataPoints(DataPointContainer const& dataPoints);
|
||||
|
||||
enum class Interface : unsigned {
|
||||
Invalid,
|
||||
Uart,
|
||||
Transceiver
|
||||
};
|
||||
|
||||
Interface getInterface() const;
|
||||
|
||||
enum class ReadState : unsigned {
|
||||
Idle,
|
||||
WaitingForFrameStart,
|
||||
FrameStartReceived,
|
||||
StartMarkerReceived,
|
||||
FrameLengthMsbReceived,
|
||||
ReadingFrame
|
||||
};
|
||||
ReadState _readState;
|
||||
void setReadState(ReadState state) {
|
||||
_readState = state;
|
||||
}
|
||||
|
||||
bool _verboseLogging = true;
|
||||
int8_t _rxEnablePin = -1;
|
||||
int8_t _txEnablePin = -1;
|
||||
Status _lastStatus = Status::Initializing;
|
||||
uint32_t _lastStatusPrinted = 0;
|
||||
uint32_t _lastRequest = 0;
|
||||
uint16_t _frameLength = 0;
|
||||
uint8_t _protocolVersion = -1;
|
||||
SerialResponse::tData _buffer = {};
|
||||
std::shared_ptr<JkBmsBatteryStats> _stats =
|
||||
std::make_shared<JkBmsBatteryStats>();
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
250
include/JkBmsDataPoints.h
Normal file
250
include/JkBmsDataPoints.h
Normal file
@ -0,0 +1,250 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
enum class DataPointLabel : uint8_t {
|
||||
CellsMilliVolt = 0x79,
|
||||
BmsTempCelsius = 0x80,
|
||||
BatteryTempOneCelsius = 0x81,
|
||||
BatteryTempTwoCelsius = 0x82,
|
||||
BatteryVoltageMilliVolt = 0x83,
|
||||
BatteryCurrentMilliAmps = 0x84,
|
||||
BatterySoCPercent = 0x85,
|
||||
BatteryTemperatureSensorAmount = 0x86,
|
||||
BatteryCycles = 0x87,
|
||||
BatteryCycleCapacity = 0x89,
|
||||
BatteryCellAmount = 0x8a,
|
||||
AlarmsBitmask = 0x8b,
|
||||
StatusBitmask = 0x8c,
|
||||
TotalOvervoltageThresholdMilliVolt = 0x8e,
|
||||
TotalUndervoltageThresholdMilliVolt = 0x8f,
|
||||
CellOvervoltageThresholdMilliVolt = 0x90,
|
||||
CellOvervoltageRecoveryMilliVolt = 0x91,
|
||||
CellOvervoltageProtectionDelaySeconds = 0x92,
|
||||
CellUndervoltageThresholdMilliVolt = 0x93,
|
||||
CellUndervoltageRecoveryMilliVolt = 0x94,
|
||||
CellUndervoltageProtectionDelaySeconds = 0x95,
|
||||
CellVoltageDiffThresholdMilliVolt = 0x96,
|
||||
DischargeOvercurrentThresholdAmperes = 0x97,
|
||||
DischargeOvercurrentDelaySeconds = 0x98,
|
||||
ChargeOvercurrentThresholdAmps = 0x99,
|
||||
ChargeOvercurrentDelaySeconds = 0x9a,
|
||||
BalanceCellVoltageThresholdMilliVolt = 0x9b,
|
||||
BalanceVoltageDiffThresholdMilliVolt = 0x9c,
|
||||
BalancingEnabled = 0x9d,
|
||||
BmsTempProtectionThresholdCelsius = 0x9e,
|
||||
BmsTempRecoveryThresholdCelsius = 0x9f,
|
||||
BatteryTempProtectionThresholdCelsius = 0xa0,
|
||||
BatteryTempRecoveryThresholdCelsius = 0xa1,
|
||||
BatteryTempDiffThresholdCelsius = 0xa2,
|
||||
ChargeHighTempThresholdCelsius = 0xa3,
|
||||
DischargeHighTempThresholdCelsius = 0xa4,
|
||||
ChargeLowTempThresholdCelsius = 0xa5,
|
||||
ChargeLowTempRecoveryCelsius = 0xa6,
|
||||
DischargeLowTempThresholdCelsius = 0xa7,
|
||||
DischargeLowTempRecoveryCelsius = 0xa8,
|
||||
CellAmountSetting = 0xa9,
|
||||
BatteryCapacitySettingAmpHours = 0xaa,
|
||||
BatteryChargeEnabled = 0xab,
|
||||
BatteryDischargeEnabled = 0xac,
|
||||
CurrentCalibrationMilliAmps = 0xad,
|
||||
BmsAddress = 0xae,
|
||||
BatteryType = 0xaf,
|
||||
SleepWaitTime = 0xb0, // what's this?
|
||||
LowCapacityAlarmThresholdPercent = 0xb1,
|
||||
ModificationPassword = 0xb2,
|
||||
DedicatedChargerSwitch = 0xb3, // what's this?
|
||||
EquipmentId = 0xb4,
|
||||
DateOfManufacturing = 0xb5,
|
||||
BmsHourMeterMinutes = 0xb6,
|
||||
BmsSoftwareVersion = 0xb7,
|
||||
CurrentCalibration = 0xb8,
|
||||
ActualBatteryCapacityAmpHours = 0xb9,
|
||||
ProductId = 0xba,
|
||||
ProtocolVersion = 0xc0
|
||||
};
|
||||
|
||||
using tCells = std::map<uint8_t, uint16_t>;
|
||||
|
||||
template<DataPointLabel> struct DataPointLabelTraits;
|
||||
|
||||
#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
|
||||
using type = t; \
|
||||
static constexpr char const name[] = #n; \
|
||||
static constexpr char const unit[] = u; \
|
||||
};
|
||||
|
||||
/**
|
||||
* the types associated with the labels are the types for the respective data
|
||||
* points in the JkBms::DataPoint class. they are *not* always equal to the
|
||||
* type used in the serial message.
|
||||
*
|
||||
* it is unfortunate that we have to repeat all enum values here to define the
|
||||
* traits. code generation could help here (labels are defined in a single
|
||||
* source of truth and this code is generated -- no typing errors, etc.).
|
||||
* however, the compiler will complain if an enum is misspelled or traits are
|
||||
* defined for a removed enum, so we will notice. it will also complain when a
|
||||
* trait is missing and if a data point for a label without traits is added to
|
||||
* the DataPointContainer class, because the traits must be available then.
|
||||
* even though this is tedious to maintain, human errors will be caught.
|
||||
*/
|
||||
LABEL_TRAIT(CellsMilliVolt, tCells, "mV");
|
||||
LABEL_TRAIT(BmsTempCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempOneCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempTwoCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryVoltageMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(BatteryCurrentMilliAmps, int32_t, "mA");
|
||||
LABEL_TRAIT(BatterySoCPercent, uint8_t, "%");
|
||||
LABEL_TRAIT(BatteryTemperatureSensorAmount, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryCycles, uint16_t, "");
|
||||
LABEL_TRAIT(BatteryCycleCapacity, uint32_t, "Ah");
|
||||
LABEL_TRAIT(BatteryCellAmount, uint16_t, "");
|
||||
LABEL_TRAIT(AlarmsBitmask, uint16_t, "");
|
||||
LABEL_TRAIT(StatusBitmask, uint16_t, "");
|
||||
LABEL_TRAIT(TotalOvervoltageThresholdMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(TotalUndervoltageThresholdMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageRecoveryMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageProtectionDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(CellUndervoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellUndervoltageRecoveryMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellUndervoltageProtectionDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(CellVoltageDiffThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(DischargeOvercurrentThresholdAmperes, uint16_t, "A");
|
||||
LABEL_TRAIT(DischargeOvercurrentDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(ChargeOvercurrentThresholdAmps, uint16_t, "A");
|
||||
LABEL_TRAIT(ChargeOvercurrentDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(BalanceCellVoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(BalanceVoltageDiffThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(BalancingEnabled, bool, "");
|
||||
LABEL_TRAIT(BmsTempProtectionThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BmsTempRecoveryThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempProtectionThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempRecoveryThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempDiffThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(ChargeHighTempThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(DischargeHighTempThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(ChargeLowTempThresholdCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(ChargeLowTempRecoveryCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(DischargeLowTempThresholdCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(DischargeLowTempRecoveryCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(CellAmountSetting, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah");
|
||||
LABEL_TRAIT(BatteryChargeEnabled, bool, "");
|
||||
LABEL_TRAIT(BatteryDischargeEnabled, bool, "");
|
||||
LABEL_TRAIT(CurrentCalibrationMilliAmps, uint16_t, "mA");
|
||||
LABEL_TRAIT(BmsAddress, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryType, uint8_t, "");
|
||||
LABEL_TRAIT(SleepWaitTime, uint16_t, "s");
|
||||
LABEL_TRAIT(LowCapacityAlarmThresholdPercent, uint8_t, "%");
|
||||
LABEL_TRAIT(ModificationPassword, std::string, "");
|
||||
LABEL_TRAIT(DedicatedChargerSwitch, bool, "");
|
||||
LABEL_TRAIT(EquipmentId, std::string, "");
|
||||
LABEL_TRAIT(DateOfManufacturing, std::string, "");
|
||||
LABEL_TRAIT(BmsHourMeterMinutes, uint32_t, "min");
|
||||
LABEL_TRAIT(BmsSoftwareVersion, std::string, "");
|
||||
LABEL_TRAIT(CurrentCalibration, bool, "");
|
||||
LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah");
|
||||
LABEL_TRAIT(ProductId, std::string, "");
|
||||
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
|
||||
#undef LABEL_TRAIT
|
||||
|
||||
class DataPoint {
|
||||
friend class DataPointContainer;
|
||||
|
||||
public:
|
||||
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
|
||||
int16_t, int32_t, std::string, tCells>;
|
||||
|
||||
DataPoint() = delete;
|
||||
|
||||
DataPoint(DataPoint const& other)
|
||||
: _strLabel(other._strLabel)
|
||||
, _strValue(other._strValue)
|
||||
, _strUnit(other._strUnit)
|
||||
, _value(other._value)
|
||||
, _timestamp(other._timestamp) { }
|
||||
|
||||
DataPoint(std::string const& strLabel, std::string const& strValue,
|
||||
std::string const& strUnit, tValue value, uint32_t timestamp)
|
||||
: _strLabel(strLabel)
|
||||
, _strValue(strValue)
|
||||
, _strUnit(strUnit)
|
||||
, _value(std::move(value))
|
||||
, _timestamp(timestamp) { }
|
||||
|
||||
std::string const& getLabelText() const { return _strLabel; }
|
||||
std::string const& getValueText() const { return _strValue; }
|
||||
std::string const& getUnitText() const { return _strUnit; }
|
||||
uint32_t getTimestamp() const { return _timestamp; }
|
||||
|
||||
private:
|
||||
std::string _strLabel;
|
||||
std::string _strValue;
|
||||
std::string _strUnit;
|
||||
tValue _value;
|
||||
uint32_t _timestamp;
|
||||
};
|
||||
|
||||
template<typename T> std::string dataPointValueToStr(T const& v);
|
||||
|
||||
class DataPointContainer {
|
||||
public:
|
||||
DataPointContainer() = default;
|
||||
|
||||
using Label = DataPointLabel;
|
||||
template<Label L> using Traits = JkBms::DataPointLabelTraits<L>;
|
||||
|
||||
template<Label L>
|
||||
void add(typename Traits<L>::type val) {
|
||||
_dataPoints.emplace(
|
||||
L,
|
||||
DataPoint(
|
||||
Traits<L>::name,
|
||||
dataPointValueToStr(val),
|
||||
Traits<L>::unit,
|
||||
DataPoint::tValue(std::move(val)),
|
||||
millis()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// make sure add() is only called with the type expected for the
|
||||
// respective label, no implicit conversions allowed.
|
||||
template<Label L, typename T>
|
||||
void add(T) = delete;
|
||||
|
||||
template<Label L>
|
||||
std::optional<DataPoint const> getDataPointFor() const {
|
||||
auto it = _dataPoints.find(L);
|
||||
if (it == _dataPoints.end()) { return std::nullopt; }
|
||||
return it->second;
|
||||
}
|
||||
|
||||
template<Label L>
|
||||
std::optional<typename Traits<L>::type> get() const {
|
||||
auto optionalDataPoint = getDataPointFor<L>();
|
||||
if (!optionalDataPoint.has_value()) { return std::nullopt; }
|
||||
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
|
||||
}
|
||||
|
||||
using tMap = std::unordered_map<Label, DataPoint const>;
|
||||
tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
|
||||
tMap::const_iterator cend() const { return _dataPoints.cend(); }
|
||||
|
||||
// copy all data points from source into this instance, overwriting
|
||||
// existing data points in this instance.
|
||||
void updateFrom(DataPointContainer const& source);
|
||||
|
||||
private:
|
||||
tMap _dataPoints;
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
93
include/JkBmsSerialMessage.h
Normal file
93
include/JkBmsSerialMessage.h
Normal file
@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
class SerialMessage {
|
||||
public:
|
||||
using tData = std::vector<uint8_t>;
|
||||
|
||||
SerialMessage() = delete;
|
||||
|
||||
enum class Command : uint8_t {
|
||||
Activate = 0x01,
|
||||
Write = 0x02,
|
||||
Read = 0x03,
|
||||
Password = 0x05,
|
||||
ReadAll = 0x06
|
||||
};
|
||||
|
||||
Command getCommand() const { return static_cast<Command>(_raw[8]); }
|
||||
|
||||
enum class Source : uint8_t {
|
||||
BMS = 0x00,
|
||||
Bluetooth = 0x01,
|
||||
GPS = 0x02,
|
||||
Host = 0x03
|
||||
};
|
||||
Source getSource() const { return static_cast<Source>(_raw[9]); }
|
||||
|
||||
enum class Type : uint8_t {
|
||||
Command = 0x00,
|
||||
Response = 0x01,
|
||||
Unsolicited = 0x02
|
||||
};
|
||||
Type getType() const { return static_cast<Type>(_raw[10]); }
|
||||
|
||||
// this does *not* include the two byte start marker
|
||||
uint16_t getFrameLength() const { return get<uint16_t>(_raw.cbegin()+2); }
|
||||
|
||||
uint32_t getTerminalId() const { return get<uint32_t>(_raw.cbegin()+4); }
|
||||
|
||||
// there are 20 bytes of overhead. two of those are the start marker
|
||||
// bytes, which are *not* counted by the frame length.
|
||||
uint16_t getVariableFieldLength() const { return getFrameLength() - 18; }
|
||||
|
||||
// the upper byte of the 4-byte "record number" is reserved (for encryption)
|
||||
uint32_t getSequence() const { return get<uint32_t>(_raw.cend()-9) >> 8; }
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
uint8_t const* data() { return _raw.data(); }
|
||||
size_t size() { return _raw.size(); }
|
||||
|
||||
protected:
|
||||
template <typename... Args>
|
||||
explicit SerialMessage(Args&&... args) : _raw(std::forward<Args>(args)...) { }
|
||||
|
||||
template<typename T, typename It> T get(It&& pos) const;
|
||||
template<typename It> bool getBool(It&& pos) const;
|
||||
template<typename It> int16_t getTemperature(It&& pos) const;
|
||||
template<typename It> std::string getString(It&& pos, size_t len, bool replaceZeroes = false) const;
|
||||
void processBatteryCurrent(tData::const_iterator& pos, uint8_t protocolVersion);
|
||||
template<typename T> void set(tData::iterator const& pos, T val);
|
||||
uint16_t calcChecksum() const;
|
||||
void updateChecksum();
|
||||
|
||||
tData _raw;
|
||||
JkBms::DataPointContainer _dp;
|
||||
|
||||
static constexpr uint16_t startMarker = 0x4e57;
|
||||
static constexpr uint8_t endMarker = 0x68;
|
||||
};
|
||||
|
||||
class SerialResponse : public SerialMessage {
|
||||
public:
|
||||
using tData = SerialMessage::tData;
|
||||
explicit SerialResponse(tData&& raw, uint8_t protocolVersion = -1);
|
||||
|
||||
DataPointContainer const& getDataPoints() const { return _dp; }
|
||||
};
|
||||
|
||||
class SerialCommand : public SerialMessage {
|
||||
public:
|
||||
using Command = SerialMessage::Command;
|
||||
explicit SerialCommand(Command cmd);
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
@ -41,7 +41,9 @@ struct PinMapping_t {
|
||||
int8_t victron_tx;
|
||||
int8_t victron_rx;
|
||||
int8_t battery_rx;
|
||||
int8_t battery_rxen;
|
||||
int8_t battery_tx;
|
||||
int8_t battery_txen;
|
||||
int8_t huawei_miso;
|
||||
int8_t huawei_mosi;
|
||||
int8_t huawei_clk;
|
||||
@ -61,7 +63,6 @@ public:
|
||||
bool isValidCmt2300Config();
|
||||
bool isValidEthConfig();
|
||||
bool isValidVictronConfig();
|
||||
bool isValidBatteryConfig();
|
||||
bool isValidHuaweiConfig();
|
||||
|
||||
private:
|
||||
|
||||
@ -2,40 +2,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <driver/twai.h>
|
||||
#include <Arduino.h>
|
||||
#include <memory>
|
||||
|
||||
#ifndef PYLONTECH_PIN_RX
|
||||
#define PYLONTECH_PIN_RX 27
|
||||
#endif
|
||||
|
||||
#ifndef PYLONTECH_PIN_TX
|
||||
#define PYLONTECH_PIN_TX 26
|
||||
#endif
|
||||
|
||||
class PylontechCanReceiverClass {
|
||||
class PylontechCanReceiver : public BatteryProvider {
|
||||
public:
|
||||
void init(int8_t rx, int8_t tx);
|
||||
void enable();
|
||||
void disable();
|
||||
void loop();
|
||||
void parseCanPackets();
|
||||
void mqtt();
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
uint16_t readUnsignedInt16(uint8_t *data);
|
||||
int16_t readSignedInt16(uint8_t *data);
|
||||
void readString(char* str, uint8_t numBytes);
|
||||
void readBooleanBits8(bool* b, uint8_t numBits);
|
||||
float scaleValue(int16_t value, float factor);
|
||||
bool getBit(uint8_t value, uint8_t bit);
|
||||
|
||||
bool _isEnabled = false;
|
||||
uint32_t _lastPublish;
|
||||
twai_general_config_t g_config;
|
||||
esp_err_t twaiLastResult;
|
||||
};
|
||||
void dummyData();
|
||||
|
||||
extern PylontechCanReceiverClass PylontechCanReceiver;
|
||||
bool _verboseLogging = true;
|
||||
std::shared_ptr<PylontechBatteryStats> _stats =
|
||||
std::make_shared<PylontechBatteryStats>();
|
||||
};
|
||||
|
||||
@ -27,8 +27,7 @@
|
||||
#include "WebApi_vedirect.h"
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "WebApi_ws_Pylontech.h"
|
||||
#include "WebApi_Pylontech.h"
|
||||
#include "WebApi_ws_battery.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiClass {
|
||||
@ -72,9 +71,7 @@ private:
|
||||
WebApiVedirectClass _webApiVedirect;
|
||||
WebApiHuaweiClass _webApiHuaweiClass;
|
||||
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
|
||||
WebApiPylontechClass _webApiPylontechClass;
|
||||
WebApiWsPylontechLiveClass _webApiWsPylontechLive;
|
||||
|
||||
WebApiWsBatteryLiveClass _webApiWsBatteryLive;
|
||||
};
|
||||
|
||||
extern WebApiClass WebApi;
|
||||
@ -1,17 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiPylontechClass {
|
||||
public:
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
void getJsonData(JsonObject& root);
|
||||
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -4,9 +4,9 @@
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiWsPylontechLiveClass {
|
||||
class WebApiWsBatteryLiveClass {
|
||||
public:
|
||||
WebApiWsPylontechLiveClass();
|
||||
WebApiWsBatteryLiveClass();
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
|
||||
@ -20,4 +20,5 @@ private:
|
||||
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
static constexpr uint16_t _responseSize = 1024 + 512;
|
||||
};
|
||||
@ -129,6 +129,9 @@
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
|
||||
|
||||
#define BATTERY_ENABLED false
|
||||
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
|
||||
#define BATTERY_JKBMS_INTERFACE 0
|
||||
#define BATTERY_JKBMS_POLLING_INTERVAL 5
|
||||
|
||||
#define HUAWEI_ENABLED false
|
||||
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
|
||||
|
||||
@ -1,4 +1,69 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "JkBmsController.h"
|
||||
|
||||
BatteryClass Battery;
|
||||
|
||||
std::shared_ptr<BatteryStats const> BatteryClass::getStats() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (!_upProvider) {
|
||||
static auto sspDummyStats = std::make_shared<BatteryStats>();
|
||||
return sspDummyStats;
|
||||
}
|
||||
|
||||
return _upProvider->getStats();
|
||||
}
|
||||
|
||||
void BatteryClass::init()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (_upProvider) {
|
||||
_upProvider->deinit();
|
||||
_upProvider = nullptr;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Battery_Enabled) { return; }
|
||||
|
||||
bool verboseLogging = config.Battery_VerboseLogging;
|
||||
|
||||
switch (config.Battery_Provider) {
|
||||
case 0:
|
||||
_upProvider = std::make_unique<PylontechCanReceiver>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
case 1:
|
||||
_upProvider = std::make_unique<JkBms::Controller>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
default:
|
||||
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (!_upProvider) { return; }
|
||||
|
||||
_upProvider->loop();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!MqttSettings.getConnected()
|
||||
|| (millis() - _lastMqttPublish) < (config.Mqtt_PublishInterval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_upProvider->getStats()->mqttPublish();
|
||||
|
||||
_lastMqttPublish = millis();
|
||||
}
|
||||
|
||||
174
src/BatteryStats.cpp
Normal file
174
src/BatteryStats.cpp
Normal file
@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "BatteryStats.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
template<typename T>
|
||||
void BatteryStats::addLiveViewValue(JsonVariant& root, std::string const& name,
|
||||
T&& value, std::string const& unit, uint8_t precision) const
|
||||
{
|
||||
auto jsonValue = root["values"][name];
|
||||
jsonValue["v"] = value;
|
||||
jsonValue["u"] = unit;
|
||||
jsonValue["d"] = precision;
|
||||
}
|
||||
|
||||
void BatteryStats::addLiveViewText(JsonVariant& root, std::string const& name,
|
||||
std::string const& text) const
|
||||
{
|
||||
root["values"][name] = text;
|
||||
}
|
||||
|
||||
void BatteryStats::addLiveViewWarning(JsonVariant& root, std::string const& name,
|
||||
bool warning) const
|
||||
{
|
||||
if (!warning) { return; }
|
||||
root["issues"][name] = 1;
|
||||
}
|
||||
|
||||
void BatteryStats::addLiveViewAlarm(JsonVariant& root, std::string const& name,
|
||||
bool alarm) const
|
||||
{
|
||||
if (!alarm) { return; }
|
||||
root["issues"][name] = 2;
|
||||
}
|
||||
|
||||
void BatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
{
|
||||
root[F("manufacturer")] = _manufacturer;
|
||||
root[F("data_age")] = getAgeSeconds();
|
||||
|
||||
addLiveViewValue(root, "SoC", _SoC, "%", 0);
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
{
|
||||
BatteryStats::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, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
|
||||
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
|
||||
addLiveViewValue(root, "voltage", _voltage, "V", 2);
|
||||
addLiveViewValue(root, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
|
||||
|
||||
addLiveViewText(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
|
||||
addLiveViewText(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no"));
|
||||
addLiveViewText(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 JkBmsBatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
{
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
|
||||
if (oVoltage.has_value()) {
|
||||
addLiveViewValue(root, "voltage",
|
||||
static_cast<float>(*oVoltage) / 1000, "V", 2);
|
||||
}
|
||||
|
||||
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
|
||||
if (oCurrent.has_value()) {
|
||||
addLiveViewValue(root, "current",
|
||||
static_cast<float>(*oCurrent) / 1000, "A", 2);
|
||||
}
|
||||
|
||||
auto oTemperature = _dataPoints.get<Label::BatteryTempOneCelsius>();
|
||||
if (oTemperature.has_value()) {
|
||||
addLiveViewValue(root, "temperature", *oTemperature, "°C", 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryStats::mqttPublish() const
|
||||
{
|
||||
MqttSettings.publish(F("battery/manufacturer"), _manufacturer);
|
||||
MqttSettings.publish(F("battery/dataAge"), String(getAgeSeconds()));
|
||||
MqttSettings.publish(F("battery/stateOfCharge"), String(_SoC));
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::mqttPublish() const
|
||||
{
|
||||
BatteryStats::mqttPublish();
|
||||
|
||||
MqttSettings.publish(F("battery/settings/chargeVoltage"), String(_chargeVoltage));
|
||||
MqttSettings.publish(F("battery/settings/chargeCurrentLimitation"), String(_chargeCurrentLimitation));
|
||||
MqttSettings.publish(F("battery/settings/dischargeCurrentLimitation"), String(_dischargeCurrentLimitation));
|
||||
MqttSettings.publish(F("battery/stateOfHealth"), String(_stateOfHealth));
|
||||
MqttSettings.publish(F("battery/voltage"), String(_voltage));
|
||||
MqttSettings.publish(F("battery/current"), String(_current));
|
||||
MqttSettings.publish(F("battery/temperature"), String(_temperature));
|
||||
MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge));
|
||||
MqttSettings.publish(F("battery/alarm/overCurrentCharge"), String(_alarmOverCurrentCharge));
|
||||
MqttSettings.publish(F("battery/alarm/underTemperature"), String(_alarmUnderTemperature));
|
||||
MqttSettings.publish(F("battery/alarm/overTemperature"), String(_alarmOverTemperature));
|
||||
MqttSettings.publish(F("battery/alarm/underVoltage"), String(_alarmUnderVoltage));
|
||||
MqttSettings.publish(F("battery/alarm/overVoltage"), String(_alarmOverVoltage));
|
||||
MqttSettings.publish(F("battery/alarm/bmsInternal"), String(_alarmBmsInternal));
|
||||
MqttSettings.publish(F("battery/warning/highCurrentDischarge"), String(_warningHighCurrentDischarge));
|
||||
MqttSettings.publish(F("battery/warning/highCurrentCharge"), String(_warningHighCurrentCharge));
|
||||
MqttSettings.publish(F("battery/warning/lowTemperature"), String(_warningLowTemperature));
|
||||
MqttSettings.publish(F("battery/warning/highTemperature"), String(_warningHighTemperature));
|
||||
MqttSettings.publish(F("battery/warning/lowVoltage"), String(_warningLowVoltage));
|
||||
MqttSettings.publish(F("battery/warning/highVoltage"), String(_warningHighVoltage));
|
||||
MqttSettings.publish(F("battery/warning/bmsInternal"), String(_warningBmsInternal));
|
||||
MqttSettings.publish(F("battery/charging/chargeEnabled"), String(_chargeEnabled));
|
||||
MqttSettings.publish(F("battery/charging/dischargeEnabled"), String(_dischargeEnabled));
|
||||
MqttSettings.publish(F("battery/charging/chargeImmediately"), String(_chargeImmediately));
|
||||
}
|
||||
|
||||
void JkBmsBatteryStats::mqttPublish() const
|
||||
{
|
||||
BatteryStats::mqttPublish();
|
||||
}
|
||||
|
||||
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||
{
|
||||
_dataPoints.updateFrom(dp);
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
_manufacturer = "JKBMS";
|
||||
auto oProductId = _dataPoints.get<Label::ProductId>();
|
||||
if (oProductId.has_value()) {
|
||||
_manufacturer = oProductId->c_str();
|
||||
auto pos = oProductId->rfind("JK");
|
||||
if (pos != std::string::npos) {
|
||||
_manufacturer = oProductId->substr(pos).c_str();
|
||||
}
|
||||
}
|
||||
|
||||
auto oSoCValue = _dataPoints.get<Label::BatterySoCPercent>();
|
||||
if (oSoCValue.has_value()) {
|
||||
_SoC = *oSoCValue;
|
||||
auto oSoCDataPoint = _dataPoints.getDataPointFor<Label::BatterySoCPercent>();
|
||||
_lastUpdateSoC = oSoCDataPoint->getTimestamp();
|
||||
}
|
||||
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
@ -178,6 +178,10 @@ bool ConfigurationClass::write()
|
||||
|
||||
JsonObject battery = doc.createNestedObject("battery");
|
||||
battery["enabled"] = config.Battery_Enabled;
|
||||
battery["verbose_logging"] = config.Battery_VerboseLogging;
|
||||
battery["provider"] = config.Battery_Provider;
|
||||
battery["jkbms_interface"] = config.Battery_JkBmsInterface;
|
||||
battery["jkbms_polling_interval"] = config.Battery_JkBmsPollingInterval;
|
||||
|
||||
JsonObject huawei = doc.createNestedObject("huawei");
|
||||
huawei["enabled"] = config.Huawei_Enabled;
|
||||
@ -392,6 +396,10 @@ bool ConfigurationClass::read()
|
||||
|
||||
JsonObject battery = doc["battery"];
|
||||
config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED;
|
||||
config.Battery_VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING;
|
||||
config.Battery_Provider = battery["provider"] | BATTERY_PROVIDER;
|
||||
config.Battery_JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
|
||||
config.Battery_JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
|
||||
|
||||
JsonObject huawei = doc["huawei"];
|
||||
config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
||||
|
||||
352
src/JkBmsController.cpp
Normal file
352
src/JkBmsController.cpp
Normal file
@ -0,0 +1,352 @@
|
||||
#include <Arduino.h>
|
||||
#include "Configuration.h"
|
||||
#include "HardwareSerial.h"
|
||||
#include "PinMapping.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
#include "JkBmsController.h"
|
||||
#include <map>
|
||||
|
||||
//#define JKBMS_DUMMY_SERIAL
|
||||
|
||||
#ifdef JKBMS_DUMMY_SERIAL
|
||||
class DummySerial {
|
||||
public:
|
||||
DummySerial() = default;
|
||||
void begin(uint32_t, uint32_t, int8_t, int8_t) {
|
||||
MessageOutput.println("JK BMS Dummy Serial: begin()");
|
||||
}
|
||||
void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); }
|
||||
void flush() { }
|
||||
bool availableForWrite() const { return true; }
|
||||
size_t write(const uint8_t *buffer, size_t size) {
|
||||
MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size);
|
||||
_byte_idx = 0;
|
||||
_msg_idx = (_msg_idx + 1) % _data.size();
|
||||
return size;
|
||||
}
|
||||
bool available() const {
|
||||
return _byte_idx < _data[_msg_idx].size();
|
||||
}
|
||||
int read() {
|
||||
if (_byte_idx >= _data[_msg_idx].size()) { return 0; }
|
||||
return _data[_msg_idx][_byte_idx++];
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::vector<uint8_t>> const _data =
|
||||
{
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb,
|
||||
0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c,
|
||||
0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07,
|
||||
0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01,
|
||||
0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c,
|
||||
0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f,
|
||||
0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a,
|
||||
0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14,
|
||||
0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02,
|
||||
0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
|
||||
0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
|
||||
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x53, 0xbb
|
||||
},
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0,
|
||||
0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c,
|
||||
0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07,
|
||||
0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba,
|
||||
0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c,
|
||||
0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f,
|
||||
0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b,
|
||||
0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14,
|
||||
0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02,
|
||||
0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
|
||||
0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
|
||||
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x4f, 0xc1
|
||||
}
|
||||
};
|
||||
size_t _msg_idx = 0;
|
||||
size_t _byte_idx = 0;
|
||||
};
|
||||
DummySerial HwSerial;
|
||||
#else
|
||||
HardwareSerial HwSerial(2);
|
||||
#endif
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
bool Controller::init(bool verboseLogging)
|
||||
{
|
||||
_verboseLogging = verboseLogging;
|
||||
|
||||
std::string ifcType = "transceiver";
|
||||
if (Interface::Transceiver != getInterface()) { ifcType = "TTL-UART"; }
|
||||
MessageOutput.printf("[JK BMS] Initialize %s interface...\r\n", ifcType.c_str());
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.printf("[JK BMS] rx = %d, rxen = %d, tx = %d, txen = %d\r\n",
|
||||
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
|
||||
|
||||
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
|
||||
MessageOutput.println(F("[JK BMS] Invalid RX/TX pin config"));
|
||||
return false;
|
||||
}
|
||||
|
||||
HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx);
|
||||
HwSerial.flush();
|
||||
|
||||
if (Interface::Transceiver != getInterface()) { return true; }
|
||||
|
||||
_rxEnablePin = pin.battery_rxen;
|
||||
_txEnablePin = pin.battery_txen;
|
||||
|
||||
if (_rxEnablePin < 0 || _txEnablePin < 0) {
|
||||
MessageOutput.println(F("[JK BMS] Invalid transceiver pin config"));
|
||||
return false;
|
||||
}
|
||||
|
||||
pinMode(_rxEnablePin, OUTPUT);
|
||||
pinMode(_txEnablePin, OUTPUT);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::deinit()
|
||||
{
|
||||
HwSerial.end();
|
||||
|
||||
if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); }
|
||||
if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); }
|
||||
}
|
||||
|
||||
Controller::Interface Controller::getInterface() const
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (0x00 == config.Battery_JkBmsInterface) { return Interface::Uart; }
|
||||
if (0x01 == config.Battery_JkBmsInterface) { return Interface::Transceiver; }
|
||||
return Interface::Invalid;
|
||||
}
|
||||
|
||||
std::string const& Controller::getStatusText(Controller::Status status)
|
||||
{
|
||||
static const std::string missing = "programmer error: missing status text";
|
||||
|
||||
static const std::map<Status, const std::string> texts = {
|
||||
{ Status::Timeout, "timeout wating for response from BMS" },
|
||||
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
|
||||
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
|
||||
{ Status::BusyReading, "busy waiting for or reading a message from the BMS" },
|
||||
{ Status::RequestSent, "request for data sent" },
|
||||
{ Status::FrameCompleted, "a whole frame was received" }
|
||||
};
|
||||
|
||||
auto iter = texts.find(status);
|
||||
if (iter == texts.end()) { return missing; }
|
||||
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
void Controller::announceStatus(Controller::Status status)
|
||||
{
|
||||
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
|
||||
|
||||
MessageOutput.printf("[%11.3f] JK BMS: %s\r\n",
|
||||
static_cast<double>(millis())/1000, getStatusText(status).c_str());
|
||||
|
||||
_lastStatus = status;
|
||||
_lastStatusPrinted = millis();
|
||||
}
|
||||
|
||||
void Controller::sendRequest(uint8_t pollInterval)
|
||||
{
|
||||
if (ReadState::Idle != _readState) {
|
||||
return announceStatus(Status::BusyReading);
|
||||
}
|
||||
|
||||
if ((millis() - _lastRequest) < pollInterval * 1000) {
|
||||
return announceStatus(Status::WaitingForPollInterval);
|
||||
}
|
||||
|
||||
if (!HwSerial.availableForWrite()) {
|
||||
return announceStatus(Status::HwSerialNotAvailableForWrite);
|
||||
}
|
||||
|
||||
SerialCommand readAll(SerialCommand::Command::ReadAll);
|
||||
|
||||
if (Interface::Transceiver == getInterface()) {
|
||||
digitalWrite(_rxEnablePin, HIGH); // disable reception (of our own data)
|
||||
digitalWrite(_txEnablePin, HIGH); // enable transmission
|
||||
}
|
||||
|
||||
HwSerial.write(readAll.data(), readAll.size());
|
||||
|
||||
if (Interface::Transceiver == getInterface()) {
|
||||
HwSerial.flush();
|
||||
digitalWrite(_rxEnablePin, LOW); // enable reception
|
||||
digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus)
|
||||
}
|
||||
|
||||
_lastRequest = millis();
|
||||
|
||||
setReadState(ReadState::WaitingForFrameStart);
|
||||
return announceStatus(Status::RequestSent);
|
||||
}
|
||||
|
||||
void Controller::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
uint8_t pollInterval = config.Battery_JkBmsPollingInterval;
|
||||
|
||||
while (HwSerial.available()) {
|
||||
rxData(HwSerial.read());
|
||||
}
|
||||
|
||||
sendRequest(pollInterval);
|
||||
|
||||
if (millis() > _lastRequest + 2 * pollInterval * 1000 + 250) {
|
||||
reset();
|
||||
return announceStatus(Status::Timeout);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::rxData(uint8_t inbyte)
|
||||
{
|
||||
_buffer.push_back(inbyte);
|
||||
|
||||
switch(_readState) {
|
||||
case ReadState::Idle: // unsolicited message from BMS
|
||||
case ReadState::WaitingForFrameStart:
|
||||
if (inbyte == 0x4E) {
|
||||
return setReadState(ReadState::FrameStartReceived);
|
||||
}
|
||||
break;
|
||||
case ReadState::FrameStartReceived:
|
||||
if (inbyte == 0x57) {
|
||||
return setReadState(ReadState::StartMarkerReceived);
|
||||
}
|
||||
break;
|
||||
case ReadState::StartMarkerReceived:
|
||||
_frameLength = inbyte << 8 | 0x00;
|
||||
return setReadState(ReadState::FrameLengthMsbReceived);
|
||||
break;
|
||||
case ReadState::FrameLengthMsbReceived:
|
||||
_frameLength |= inbyte;
|
||||
_frameLength -= 2; // length field already read
|
||||
return setReadState(ReadState::ReadingFrame);
|
||||
break;
|
||||
case ReadState::ReadingFrame:
|
||||
_frameLength--;
|
||||
if (_frameLength == 0) {
|
||||
return frameComplete();
|
||||
}
|
||||
return setReadState(ReadState::ReadingFrame);
|
||||
break;
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Controller::reset()
|
||||
{
|
||||
_buffer.clear();
|
||||
return setReadState(ReadState::Idle);
|
||||
}
|
||||
|
||||
void Controller::frameComplete()
|
||||
{
|
||||
announceStatus(Status::FrameCompleted);
|
||||
|
||||
if (_verboseLogging) {
|
||||
double ts = static_cast<double>(millis())/1000;
|
||||
MessageOutput.printf("[%11.3f] JK BMS: raw data (%d Bytes):",
|
||||
ts, _buffer.size());
|
||||
for (size_t ctr = 0; ctr < _buffer.size(); ++ctr) {
|
||||
if (ctr % 16 == 0) {
|
||||
MessageOutput.printf("\r\n[%11.3f] JK BMS: ", ts);
|
||||
}
|
||||
MessageOutput.printf("%02x ", _buffer[ctr]);
|
||||
}
|
||||
MessageOutput.println();
|
||||
}
|
||||
|
||||
auto pResponse = std::make_unique<SerialResponse>(std::move(_buffer), _protocolVersion);
|
||||
if (pResponse->isValid()) {
|
||||
processDataPoints(pResponse->getDataPoints());
|
||||
} // if invalid, error message has been produced by SerialResponse c'tor
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Controller::processDataPoints(DataPointContainer const& dataPoints)
|
||||
{
|
||||
_stats->updateFrom(dataPoints);
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
auto oProtocolVersion = dataPoints.get<Label::ProtocolVersion>();
|
||||
if (oProtocolVersion.has_value()) { _protocolVersion = *oProtocolVersion; }
|
||||
|
||||
if (!_verboseLogging) { return; }
|
||||
|
||||
auto iter = dataPoints.cbegin();
|
||||
while ( iter != dataPoints.cend() ) {
|
||||
MessageOutput.printf("[%11.3f] JK BMS: %s: %s%s\r\n",
|
||||
static_cast<double>(iter->second.getTimestamp())/1000,
|
||||
iter->second.getLabelText().c_str(),
|
||||
iter->second.getValueText().c_str(),
|
||||
iter->second.getUnitText().c_str());
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
56
src/JkBmsDataPoints.cpp
Normal file
56
src/JkBmsDataPoints.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
static char conversionBuffer[16];
|
||||
|
||||
template<typename T>
|
||||
std::string dataPointValueToStr(T const& v) {
|
||||
snprintf(conversionBuffer, sizeof(conversionBuffer), "%d", v);
|
||||
return conversionBuffer;
|
||||
}
|
||||
|
||||
// explicit instanciations for the above unspecialized implementation
|
||||
template std::string dataPointValueToStr(int16_t const& v);
|
||||
template std::string dataPointValueToStr(int32_t const& v);
|
||||
template std::string dataPointValueToStr(uint8_t const& v);
|
||||
template std::string dataPointValueToStr(uint16_t const& v);
|
||||
template std::string dataPointValueToStr(uint32_t const& v);
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(std::string const& v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(bool const& v) {
|
||||
return v?"yes":"no";
|
||||
}
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(tCells const& v) {
|
||||
std::string res;
|
||||
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
|
||||
res += "(";
|
||||
std::string sep = "";
|
||||
for(auto const& mapval : v) {
|
||||
snprintf(conversionBuffer, sizeof(conversionBuffer), "%s%d=%d",
|
||||
sep.c_str(), mapval.first, mapval.second);
|
||||
res += conversionBuffer;
|
||||
sep = ", ";
|
||||
}
|
||||
res += ")";
|
||||
return std::move(res);
|
||||
}
|
||||
|
||||
void DataPointContainer::updateFrom(DataPointContainer const& source)
|
||||
{
|
||||
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
|
||||
_dataPoints.erase(iter->first);
|
||||
_dataPoints.insert(*iter);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
363
src/JkBmsSerialMessage.cpp
Normal file
363
src/JkBmsSerialMessage.cpp
Normal file
@ -0,0 +1,363 @@
|
||||
#include <numeric>
|
||||
|
||||
#include "JkBmsSerialMessage.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
SerialCommand::SerialCommand(SerialCommand::Command cmd)
|
||||
: SerialMessage(20, 0x00)
|
||||
{
|
||||
set(_raw.begin(), startMarker);
|
||||
set(_raw.begin() + 2, static_cast<uint16_t>(_raw.size() - 2)); // frame length
|
||||
set(_raw.begin() + 8, static_cast<uint8_t>(cmd));
|
||||
set(_raw.begin() + 9, static_cast<uint8_t>(Source::Host));
|
||||
set(_raw.begin() + 10, static_cast<uint8_t>(Type::Command));
|
||||
set(_raw.end() - 5, endMarker);
|
||||
updateChecksum();
|
||||
}
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
template<Label L> using Traits = DataPointLabelTraits<L>;
|
||||
|
||||
SerialResponse::SerialResponse(tData&& raw, uint8_t protocolVersion)
|
||||
: SerialMessage(std::move(raw))
|
||||
{
|
||||
if (!isValid()) { return; }
|
||||
|
||||
auto pos = _raw.cbegin() + 11;
|
||||
auto end = pos + getVariableFieldLength();
|
||||
|
||||
while ( pos < end ) {
|
||||
uint8_t fieldType = *(pos++);
|
||||
|
||||
/**
|
||||
* there seems to be no way to make this more generic. the main reason
|
||||
* is that a non-constexpr value (fieldType cast as Label) cannot be
|
||||
* used as a template parameter.
|
||||
*/
|
||||
switch(fieldType) {
|
||||
case 0x79:
|
||||
{
|
||||
uint8_t cellAmount = *(pos++) / 3;
|
||||
std::map<uint8_t, uint16_t> voltages;
|
||||
for (size_t cellCounter = 0; cellCounter < cellAmount; ++cellCounter) {
|
||||
uint8_t idx = *(pos++);
|
||||
auto cellMilliVolt = get<uint16_t>(pos);
|
||||
voltages[idx] = cellMilliVolt;
|
||||
}
|
||||
_dp.add<Label::CellsMilliVolt>(voltages);
|
||||
break;
|
||||
}
|
||||
case 0x80:
|
||||
_dp.add<Label::BmsTempCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x81:
|
||||
_dp.add<Label::BatteryTempOneCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x82:
|
||||
_dp.add<Label::BatteryTempTwoCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x83:
|
||||
_dp.add<Label::BatteryVoltageMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x84:
|
||||
processBatteryCurrent(pos, protocolVersion);
|
||||
break;
|
||||
case 0x85:
|
||||
_dp.add<Label::BatterySoCPercent>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0x86:
|
||||
_dp.add<Label::BatteryTemperatureSensorAmount>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0x87:
|
||||
_dp.add<Label::BatteryCycles>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x89:
|
||||
_dp.add<Label::BatteryCycleCapacity>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0x8a:
|
||||
_dp.add<Label::BatteryCellAmount>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8b:
|
||||
_dp.add<Label::AlarmsBitmask>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8c:
|
||||
_dp.add<Label::StatusBitmask>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8e:
|
||||
_dp.add<Label::TotalOvervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x8f:
|
||||
_dp.add<Label::TotalUndervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x90:
|
||||
_dp.add<Label::CellOvervoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x91:
|
||||
_dp.add<Label::CellOvervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x92:
|
||||
_dp.add<Label::CellOvervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x93:
|
||||
_dp.add<Label::CellUndervoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x94:
|
||||
_dp.add<Label::CellUndervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x95:
|
||||
_dp.add<Label::CellUndervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x96:
|
||||
_dp.add<Label::CellVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x97:
|
||||
_dp.add<Label::DischargeOvercurrentThresholdAmperes>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x98:
|
||||
_dp.add<Label::DischargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x99:
|
||||
_dp.add<Label::ChargeOvercurrentThresholdAmps>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9a:
|
||||
_dp.add<Label::ChargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9b:
|
||||
_dp.add<Label::BalanceCellVoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9c:
|
||||
_dp.add<Label::BalanceVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9d:
|
||||
_dp.add<Label::BalancingEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0x9e:
|
||||
_dp.add<Label::BmsTempProtectionThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9f:
|
||||
_dp.add<Label::BmsTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa0:
|
||||
_dp.add<Label::BatteryTempProtectionThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa1:
|
||||
_dp.add<Label::BatteryTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa2:
|
||||
_dp.add<Label::BatteryTempDiffThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa3:
|
||||
_dp.add<Label::ChargeHighTempThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa4:
|
||||
_dp.add<Label::DischargeHighTempThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa5:
|
||||
_dp.add<Label::ChargeLowTempThresholdCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa6:
|
||||
_dp.add<Label::ChargeLowTempRecoveryCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa7:
|
||||
_dp.add<Label::DischargeLowTempThresholdCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa8:
|
||||
_dp.add<Label::DischargeLowTempRecoveryCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa9:
|
||||
_dp.add<Label::CellAmountSetting>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xaa:
|
||||
_dp.add<Label::BatteryCapacitySettingAmpHours>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xab:
|
||||
_dp.add<Label::BatteryChargeEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0xac:
|
||||
_dp.add<Label::BatteryDischargeEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0xad:
|
||||
_dp.add<Label::CurrentCalibrationMilliAmps>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xae:
|
||||
_dp.add<Label::BmsAddress>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xaf:
|
||||
_dp.add<Label::BatteryType>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xb0:
|
||||
_dp.add<Label::SleepWaitTime>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xb1:
|
||||
_dp.add<Label::LowCapacityAlarmThresholdPercent>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xb2:
|
||||
_dp.add<Label::ModificationPassword>(getString(pos, 10));
|
||||
break;
|
||||
case 0xb3:
|
||||
_dp.add<Label::DedicatedChargerSwitch>(getBool(pos));
|
||||
break;
|
||||
case 0xb4:
|
||||
_dp.add<Label::EquipmentId>(getString(pos, 8));
|
||||
break;
|
||||
case 0xb5:
|
||||
_dp.add<Label::DateOfManufacturing >(getString(pos, 4));
|
||||
break;
|
||||
case 0xb6:
|
||||
_dp.add<Label::BmsHourMeterMinutes>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xb7:
|
||||
_dp.add<Label::BmsSoftwareVersion>(getString(pos, 15));
|
||||
break;
|
||||
case 0xb8:
|
||||
_dp.add<Label::CurrentCalibration>(getBool(pos));
|
||||
break;
|
||||
case 0xb9:
|
||||
_dp.add<Label::ActualBatteryCapacityAmpHours>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xba:
|
||||
_dp.add<Label::ProductId>(getString(pos, 24, true));
|
||||
break;
|
||||
case 0xc0:
|
||||
_dp.add<Label::ProtocolVersion>(get<uint8_t>(pos));
|
||||
break;
|
||||
default:
|
||||
MessageOutput.printf("unknown field type 0x%02x\r\n", fieldType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE that this function moves the iterator by the amount of bytes read.
|
||||
*/
|
||||
template<typename T, typename It>
|
||||
T SerialMessage::get(It&& pos) const
|
||||
{
|
||||
// add easy-to-understand error message when called with non-const iter,
|
||||
// as compiler generated error message is hard to understand.
|
||||
using ItNoRef = typename std::remove_reference<It>::type;
|
||||
using PtrType = typename std::iterator_traits<ItNoRef>::pointer;
|
||||
using ValueType = typename std::remove_pointer<PtrType>::type;
|
||||
static_assert(std::is_const<ValueType>::value, "get() must be called with a const_iterator");
|
||||
|
||||
// avoid out-of-bound read
|
||||
if (std::distance(pos, _raw.cend()) < sizeof(T)) { return 0; }
|
||||
|
||||
T res = 0;
|
||||
for (unsigned i = 0; i < sizeof(T); ++i) {
|
||||
res |= static_cast<T>(*(pos++)) << (sizeof(T)-1-i)*8;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
bool SerialMessage::getBool(It&& pos) const
|
||||
{
|
||||
uint8_t raw = get<uint8_t>(pos);
|
||||
return raw > 0;
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
int16_t SerialMessage::getTemperature(It&& pos) const
|
||||
{
|
||||
uint16_t raw = get<uint16_t>(pos);
|
||||
if (raw <= 100) { return static_cast<int16_t>(raw); }
|
||||
return static_cast<int16_t>(raw - 100) * (-1);
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
std::string SerialMessage::getString(It&& pos, size_t len, bool replaceZeroes) const
|
||||
{
|
||||
// avoid out-of-bound read
|
||||
len = std::min<size_t>(std::distance(pos, _raw.cend()), len);
|
||||
|
||||
auto start = pos;
|
||||
pos += len;
|
||||
|
||||
if (replaceZeroes) {
|
||||
std::vector<uint8_t> copy(start, pos);
|
||||
for (auto& c : copy) {
|
||||
if (c == 0) { c = 0x20; } // replace by ASCII space
|
||||
}
|
||||
return std::string(copy.cbegin(), copy.cend());
|
||||
}
|
||||
|
||||
return std::string(start, pos);
|
||||
}
|
||||
|
||||
void SerialMessage::processBatteryCurrent(SerialMessage::tData::const_iterator& pos, uint8_t protocolVersion)
|
||||
{
|
||||
uint16_t raw = get<uint16_t>(pos);
|
||||
|
||||
if (0x00 == protocolVersion) {
|
||||
// untested!
|
||||
_dp.add<Label::BatteryCurrentMilliAmps>((static_cast<int32_t>(10000) - raw) * 10);
|
||||
return;
|
||||
}
|
||||
else if (0x01 == protocolVersion) {
|
||||
bool charging = (raw & 0x8000) > 0;
|
||||
_dp.add<Label::BatteryCurrentMilliAmps>(static_cast<int32_t>(raw & 0x7FFF) * (charging ? 10 : -10));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageOutput.println("cannot decode battery current field without knowing the protocol version");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void SerialMessage::set(tData::iterator const& pos, T val)
|
||||
{
|
||||
// avoid out-of-bound write
|
||||
if (std::distance(pos, _raw.end()) < sizeof(T)) { return; }
|
||||
|
||||
for (unsigned i = 0; i < sizeof(T); ++i) {
|
||||
*(pos+i) = static_cast<uint8_t>(val >> (sizeof(T)-1-i)*8);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t SerialMessage::calcChecksum() const
|
||||
{
|
||||
return std::accumulate(_raw.cbegin(), _raw.cend()-4, 0);
|
||||
}
|
||||
|
||||
void SerialMessage::updateChecksum()
|
||||
{
|
||||
set(_raw.end()-2, calcChecksum());
|
||||
}
|
||||
|
||||
bool SerialMessage::isValid() const {
|
||||
uint16_t const actualStartMarker = get<uint16_t>(_raw.cbegin());
|
||||
if (actualStartMarker != startMarker) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid start marker %04x, expected 0x%04x\r\n",
|
||||
actualStartMarker, startMarker);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t const frameLength = get<uint16_t>(_raw.cbegin()+2);
|
||||
if (frameLength != _raw.size() - 2) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: unexpected frame length %04x, expected 0x%04x\r\n",
|
||||
frameLength, _raw.size() - 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t const actualEndMarker = *(_raw.cend()-5);
|
||||
if (actualEndMarker != endMarker) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid end marker %02x, expected 0x%02x\r\n",
|
||||
actualEndMarker, endMarker);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t const actualChecksum = get<uint16_t>(_raw.cend()-2);
|
||||
uint16_t const expectedChecksum = calcChecksum();
|
||||
if (actualChecksum != expectedChecksum) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid checksum 0x%04x, expected 0x%04x\r\n",
|
||||
actualChecksum, expectedChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
@ -177,7 +177,7 @@ void MqttHandlePylontechHassClass::createDeviceInfo(JsonObject& object)
|
||||
object[F("ids")] = serial;
|
||||
object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString();
|
||||
object[F("mf")] = F("OpenDTU");
|
||||
object[F("mdl")] = Battery.manufacturer;
|
||||
object[F("mdl")] = Battery.getStats()->getManufacturer();
|
||||
object[F("sw")] = AUTO_GIT_HASH;
|
||||
}
|
||||
|
||||
|
||||
@ -94,12 +94,30 @@
|
||||
#define VICTRON_PIN_RX -1
|
||||
#endif
|
||||
|
||||
#ifndef PYLONTECH_PIN_RX
|
||||
#define PYLONTECH_PIN_RX -1
|
||||
#ifndef BATTERY_PIN_RX
|
||||
#define BATTERY_PIN_RX -1
|
||||
#endif
|
||||
|
||||
#ifndef PYLONTECH_PIN_TX
|
||||
#define PYLONTECH_PIN_TX -1
|
||||
#ifdef PYLONTECH_PIN_RX
|
||||
#undef BATTERY_PIN_RX
|
||||
#define BATTERY_PIN_RX PYLONTECH_PIN_RX
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_RXEN
|
||||
#define BATTERY_PIN_RXEN -1
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_TX
|
||||
#define BATTERY_PIN_TX -1
|
||||
#endif
|
||||
|
||||
#ifdef PYLONTECH_PIN_TX
|
||||
#undef BATTERY_PIN_TX
|
||||
#define BATTERY_PIN_TX PYLONTECH_PIN_TX
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_TXEN
|
||||
#define BATTERY_PIN_TXEN -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
@ -167,8 +185,10 @@ PinMappingClass::PinMappingClass()
|
||||
_pinMapping.victron_tx = VICTRON_PIN_TX;
|
||||
_pinMapping.victron_rx = VICTRON_PIN_RX;
|
||||
|
||||
_pinMapping.battery_rx = PYLONTECH_PIN_RX;
|
||||
_pinMapping.battery_tx = PYLONTECH_PIN_TX;
|
||||
_pinMapping.battery_rx = BATTERY_PIN_RX;
|
||||
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
|
||||
_pinMapping.battery_tx = BATTERY_PIN_TX;
|
||||
_pinMapping.battery_txen = BATTERY_PIN_TXEN;
|
||||
|
||||
_pinMapping.huawei_miso = HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = HUAWEI_PIN_MOSI;
|
||||
@ -240,8 +260,10 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
|
||||
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
|
||||
|
||||
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | PYLONTECH_PIN_RX;
|
||||
_pinMapping.battery_tx = doc[i]["battery"]["tx"] | PYLONTECH_PIN_TX;
|
||||
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
|
||||
_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.huawei_miso = doc[i]["huawei"]["miso"] | HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = doc[i]["huawei"]["mosi"] | HUAWEI_PIN_MOSI;
|
||||
@ -289,12 +311,6 @@ bool PinMappingClass::isValidVictronConfig()
|
||||
&& _pinMapping.victron_tx >= 0;
|
||||
}
|
||||
|
||||
bool PinMappingClass::isValidBatteryConfig()
|
||||
{
|
||||
return _pinMapping.battery_rx >= 0
|
||||
&& _pinMapping.battery_tx >= 0;
|
||||
}
|
||||
|
||||
bool PinMappingClass::isValidHuaweiConfig()
|
||||
{
|
||||
return _pinMapping.huawei_miso >= 0
|
||||
|
||||
@ -284,10 +284,12 @@ void PowerLimiterClass::loop()
|
||||
}
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %li ms\r\n",
|
||||
(config.Battery_Enabled?"enabled":"disabled"), Battery.stateOfCharge,
|
||||
config.PowerLimiter_BatterySocStartThreshold, config.PowerLimiter_BatterySocStopThreshold,
|
||||
millis() - Battery.stateOfChargeLastUpdate);
|
||||
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s\r\n",
|
||||
(config.Battery_Enabled?"enabled":"disabled"),
|
||||
Battery.getStats()->getSoC(),
|
||||
config.PowerLimiter_BatterySocStartThreshold,
|
||||
config.PowerLimiter_BatterySocStopThreshold,
|
||||
Battery.getStats()->getSoCAgeSeconds());
|
||||
|
||||
float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter_InverterChannelId, FLD_UDC);
|
||||
MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n",
|
||||
@ -598,8 +600,9 @@ bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold,
|
||||
|
||||
// prefer SoC provided through battery interface
|
||||
if (config.Battery_Enabled && socThreshold > 0.0
|
||||
&& (millis() - Battery.stateOfChargeLastUpdate) < 60000) {
|
||||
return compare(Battery.stateOfCharge, socThreshold);
|
||||
&& Battery.getStats()->isValid()
|
||||
&& Battery.getStats()->getSoCAgeSeconds() < 60) {
|
||||
return compare(Battery.getStats()->getSoC(), socThreshold);
|
||||
}
|
||||
|
||||
// use voltage threshold as fallback
|
||||
|
||||
@ -1,48 +1,53 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "Battery.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "PinMapping.h"
|
||||
#include <driver/twai.h>
|
||||
#include <ctime>
|
||||
|
||||
//#define PYLONTECH_DEBUG_ENABLED
|
||||
//#define PYLONTECH_DUMMY
|
||||
|
||||
PylontechCanReceiverClass PylontechCanReceiver;
|
||||
|
||||
void PylontechCanReceiverClass::init(int8_t rx, int8_t tx)
|
||||
bool PylontechCanReceiver::init(bool verboseLogging)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)tx, (gpio_num_t)rx, TWAI_MODE_NORMAL);
|
||||
if (config.Battery_Enabled) {
|
||||
enable();
|
||||
}
|
||||
_verboseLogging = verboseLogging;
|
||||
|
||||
MessageOutput.println(F("[Pylontech] Initialize interface..."));
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.printf("[Pylontech] Interface rx = %d, tx = %d\r\n",
|
||||
pin.battery_rx, pin.battery_tx);
|
||||
|
||||
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
|
||||
MessageOutput.println(F("[Pylontech] Invalid pin config"));
|
||||
return false;
|
||||
}
|
||||
|
||||
void PylontechCanReceiverClass::enable()
|
||||
{
|
||||
if (_isEnabled) {
|
||||
return;
|
||||
}
|
||||
auto tx = static_cast<gpio_num_t>(pin.battery_tx);
|
||||
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
|
||||
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL);
|
||||
|
||||
// Initialize configuration structures using macro initializers
|
||||
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
|
||||
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
|
||||
|
||||
// Install TWAI driver
|
||||
twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config);
|
||||
esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config);
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver installed"));
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver install - invalid arg"));
|
||||
return false;
|
||||
break;
|
||||
case ESP_ERR_NO_MEM:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver install - no memory"));
|
||||
return false;
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver install - invalid state"));
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -51,22 +56,20 @@ void PylontechCanReceiverClass::enable()
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver started"));
|
||||
_isEnabled = true;
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver start - invalid state"));
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PylontechCanReceiverClass::disable()
|
||||
void PylontechCanReceiver::deinit()
|
||||
{
|
||||
if (!_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop TWAI driver
|
||||
twaiLastResult = twai_stop();
|
||||
esp_err_t twaiLastResult = twai_stop();
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver stopped"));
|
||||
@ -81,7 +84,6 @@ void PylontechCanReceiverClass::disable()
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver uninstalled"));
|
||||
_isEnabled = false;
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println(F("[Pylontech] Twai driver uninstall - invalid state"));
|
||||
@ -89,64 +91,15 @@ void PylontechCanReceiverClass::disable()
|
||||
}
|
||||
}
|
||||
|
||||
void PylontechCanReceiverClass::loop()
|
||||
void PylontechCanReceiver::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
#ifdef PYLONTECH_DUMMY
|
||||
return dummyData();
|
||||
#endif
|
||||
|
||||
if (!config.Battery_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
parseCanPackets();
|
||||
mqtt();
|
||||
}
|
||||
|
||||
void PylontechCanReceiverClass::mqtt()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!MqttSettings.getConnected()
|
||||
|| (millis() - _lastPublish) < (config.Mqtt_PublishInterval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
String topic = "battery";
|
||||
MqttSettings.publish(topic + "/settings/chargeVoltage", String(Battery.chargeVoltage));
|
||||
MqttSettings.publish(topic + "/settings/chargeCurrentLimitation", String(Battery.chargeCurrentLimitation));
|
||||
MqttSettings.publish(topic + "/settings/dischargeCurrentLimitation", String(Battery.dischargeCurrentLimitation));
|
||||
MqttSettings.publish(topic + "/stateOfCharge", String(Battery.stateOfCharge));
|
||||
MqttSettings.publish(topic + "/stateOfHealth", String(Battery.stateOfHealth));
|
||||
MqttSettings.publish(topic + "/dataAge", String((millis() - Battery.lastUpdate) / 1000));
|
||||
MqttSettings.publish(topic + "/voltage", String(Battery.voltage));
|
||||
MqttSettings.publish(topic + "/current", String(Battery.current));
|
||||
MqttSettings.publish(topic + "/temperature", String(Battery.temperature));
|
||||
MqttSettings.publish(topic + "/alarm/overCurrentDischarge", String(Battery.alarmOverCurrentDischarge));
|
||||
MqttSettings.publish(topic + "/alarm/underTemperature", String(Battery.alarmUnderTemperature));
|
||||
MqttSettings.publish(topic + "/alarm/overTemperature", String(Battery.alarmOverTemperature));
|
||||
MqttSettings.publish(topic + "/alarm/underVoltage", String(Battery.alarmUnderVoltage));
|
||||
MqttSettings.publish(topic + "/alarm/overVoltage", String(Battery.alarmOverVoltage));
|
||||
MqttSettings.publish(topic + "/alarm/bmsInternal", String(Battery.alarmBmsInternal));
|
||||
MqttSettings.publish(topic + "/alarm/overCurrentCharge", String(Battery.alarmOverCurrentCharge));
|
||||
MqttSettings.publish(topic + "/warning/highCurrentDischarge", String(Battery.warningHighCurrentDischarge));
|
||||
MqttSettings.publish(topic + "/warning/lowTemperature", String(Battery.warningLowTemperature));
|
||||
MqttSettings.publish(topic + "/warning/highTemperature", String(Battery.warningHighTemperature));
|
||||
MqttSettings.publish(topic + "/warning/lowVoltage", String(Battery.warningLowVoltage));
|
||||
MqttSettings.publish(topic + "/warning/highVoltage", String(Battery.warningHighVoltage));
|
||||
MqttSettings.publish(topic + "/warning/bmsInternal", String(Battery.warningBmsInternal));
|
||||
MqttSettings.publish(topic + "/warning/highCurrentCharge", String(Battery.warningHighCurrentCharge));
|
||||
MqttSettings.publish(topic + "/manufacturer", Battery.manufacturer);
|
||||
MqttSettings.publish(topic + "/charging/chargeEnabled", String(Battery.chargeEnabled));
|
||||
MqttSettings.publish(topic + "/charging/dischargeEnabled", String(Battery.dischargeEnabled));
|
||||
MqttSettings.publish(topic + "/charging/chargeImmediately", String(Battery.chargeImmediately));
|
||||
}
|
||||
|
||||
void PylontechCanReceiverClass::parseCanPackets()
|
||||
{
|
||||
// Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer
|
||||
twai_status_info_t status_info;
|
||||
twaiLastResult = twai_get_status_info(&status_info);
|
||||
esp_err_t twaiLastResult = twai_get_status_info(&status_info);
|
||||
if (twaiLastResult != ESP_OK) {
|
||||
switch (twaiLastResult) {
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
@ -171,124 +124,126 @@ void PylontechCanReceiverClass::parseCanPackets()
|
||||
|
||||
switch (rx_message.identifier) {
|
||||
case 0x351: {
|
||||
Battery.chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1);
|
||||
Battery.chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
Battery.dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
_stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1);
|
||||
_stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->_dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\n",
|
||||
Battery.chargeVoltage, Battery.chargeCurrentLimitation, Battery.dischargeCurrentLimitation);
|
||||
#endif
|
||||
_stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x355: {
|
||||
Battery.stateOfCharge = this->readUnsignedInt16(rx_message.data);
|
||||
Battery.stateOfChargeLastUpdate = millis();
|
||||
Battery.stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);
|
||||
Battery.lastUpdate = millis();
|
||||
_stats->setSoC(static_cast<uint8_t>(this->readUnsignedInt16(rx_message.data)));
|
||||
_stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] soc: %d soh: %d\n",
|
||||
Battery.stateOfCharge, Battery.stateOfHealth);
|
||||
#endif
|
||||
_stats->getSoC(), _stats->_stateOfHealth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x356: {
|
||||
Battery.voltage = this->scaleValue(this->readSignedInt16(rx_message.data), 0.01);
|
||||
Battery.current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
Battery.temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
_stats->_voltage = this->scaleValue(this->readSignedInt16(rx_message.data), 0.01);
|
||||
_stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\n",
|
||||
Battery.voltage, Battery.current, Battery.temperature);
|
||||
#endif
|
||||
_stats->_voltage, _stats->_current, _stats->_temperature);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x359: {
|
||||
uint16_t alarmBits = rx_message.data[0];
|
||||
Battery.alarmOverCurrentDischarge = this->getBit(alarmBits, 7);
|
||||
Battery.alarmUnderTemperature = this->getBit(alarmBits, 4);
|
||||
Battery.alarmOverTemperature = this->getBit(alarmBits, 3);
|
||||
Battery.alarmUnderVoltage = this->getBit(alarmBits, 2);
|
||||
Battery.alarmOverVoltage= this->getBit(alarmBits, 1);
|
||||
_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];
|
||||
Battery.alarmBmsInternal= this->getBit(alarmBits, 3);
|
||||
Battery.alarmOverCurrentCharge = this->getBit(alarmBits, 0);
|
||||
_stats->_alarmBmsInternal= this->getBit(alarmBits, 3);
|
||||
_stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Alarms: %d %d %d %d %d %d %d\n",
|
||||
Battery.alarmOverCurrentDischarge,
|
||||
Battery.alarmUnderTemperature,
|
||||
Battery.alarmOverTemperature,
|
||||
Battery.alarmUnderVoltage,
|
||||
Battery.alarmOverVoltage,
|
||||
Battery.alarmBmsInternal,
|
||||
Battery.alarmOverCurrentCharge);
|
||||
#endif
|
||||
_stats->_alarmOverCurrentDischarge,
|
||||
_stats->_alarmUnderTemperature,
|
||||
_stats->_alarmOverTemperature,
|
||||
_stats->_alarmUnderVoltage,
|
||||
_stats->_alarmOverVoltage,
|
||||
_stats->_alarmBmsInternal,
|
||||
_stats->_alarmOverCurrentCharge);
|
||||
}
|
||||
|
||||
uint16_t warningBits = rx_message.data[2];
|
||||
Battery.warningHighCurrentDischarge = this->getBit(warningBits, 7);
|
||||
Battery.warningLowTemperature = this->getBit(warningBits, 4);
|
||||
Battery.warningHighTemperature = this->getBit(warningBits, 3);
|
||||
Battery.warningLowVoltage = this->getBit(warningBits, 2);
|
||||
Battery.warningHighVoltage = this->getBit(warningBits, 1);
|
||||
_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];
|
||||
Battery.warningBmsInternal= this->getBit(warningBits, 3);
|
||||
Battery.warningHighCurrentCharge = this->getBit(warningBits, 0);
|
||||
_stats->_warningBmsInternal= this->getBit(warningBits, 3);
|
||||
_stats->_warningHighCurrentCharge = this->getBit(warningBits, 0);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Warnings: %d %d %d %d %d %d %d\n",
|
||||
Battery.warningHighCurrentDischarge,
|
||||
Battery.warningLowTemperature,
|
||||
Battery.warningHighTemperature,
|
||||
Battery.warningLowVoltage,
|
||||
Battery.warningHighVoltage,
|
||||
Battery.warningBmsInternal,
|
||||
Battery.warningHighCurrentCharge);
|
||||
#endif
|
||||
_stats->_warningHighCurrentDischarge,
|
||||
_stats->_warningLowTemperature,
|
||||
_stats->_warningHighTemperature,
|
||||
_stats->_warningLowVoltage,
|
||||
_stats->_warningHighVoltage,
|
||||
_stats->_warningBmsInternal,
|
||||
_stats->_warningHighCurrentCharge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x35E: {
|
||||
String manufacturer = String(rx_message.data, rx_message.data_length_code);
|
||||
//CAN.readString();
|
||||
String manufacturer(reinterpret_cast<char*>(rx_message.data),
|
||||
rx_message.data_length_code);
|
||||
|
||||
if (manufacturer == "") {
|
||||
break;
|
||||
if (manufacturer.isEmpty()) { break; }
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Manufacturer: %s\n", manufacturer.c_str());
|
||||
}
|
||||
|
||||
strlcpy(Battery.manufacturer, manufacturer.c_str(), sizeof(Battery.manufacturer));
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
MessageOutput.printf("[Pylontech] Manufacturer: %s\n", manufacturer.c_str());
|
||||
#endif
|
||||
_stats->setManufacturer(std::move(manufacturer));
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x35C: {
|
||||
uint16_t chargeStatusBits = rx_message.data[0];
|
||||
Battery.chargeEnabled = this->getBit(chargeStatusBits, 7);
|
||||
Battery.dischargeEnabled = this->getBit(chargeStatusBits, 6);
|
||||
Battery.chargeImmediately = this->getBit(chargeStatusBits, 5);
|
||||
_stats->_chargeEnabled = this->getBit(chargeStatusBits, 7);
|
||||
_stats->_dischargeEnabled = this->getBit(chargeStatusBits, 6);
|
||||
_stats->_chargeImmediately = this->getBit(chargeStatusBits, 5);
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] chargeStatusBits: %d %d %d\n",
|
||||
Battery.chargeEnabled,
|
||||
Battery.dischargeEnabled,
|
||||
Battery.chargeImmediately);
|
||||
#endif
|
||||
_stats->_chargeEnabled,
|
||||
_stats->_dischargeEnabled,
|
||||
_stats->_chargeImmediately);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return; // do not update last update timestamp
|
||||
break;
|
||||
}
|
||||
|
||||
uint16_t PylontechCanReceiverClass::readUnsignedInt16(uint8_t *data)
|
||||
_stats->setLastUpdate(millis());
|
||||
}
|
||||
|
||||
uint16_t PylontechCanReceiver::readUnsignedInt16(uint8_t *data)
|
||||
{
|
||||
uint8_t bytes[2];
|
||||
bytes[0] = *data;
|
||||
@ -296,17 +251,94 @@ uint16_t PylontechCanReceiverClass::readUnsignedInt16(uint8_t *data)
|
||||
return (bytes[1] << 8) + bytes[0];
|
||||
}
|
||||
|
||||
int16_t PylontechCanReceiverClass::readSignedInt16(uint8_t *data)
|
||||
int16_t PylontechCanReceiver::readSignedInt16(uint8_t *data)
|
||||
{
|
||||
return this->readUnsignedInt16(data);
|
||||
}
|
||||
|
||||
float PylontechCanReceiverClass::scaleValue(int16_t value, float factor)
|
||||
float PylontechCanReceiver::scaleValue(int16_t value, float factor)
|
||||
{
|
||||
return value * factor;
|
||||
}
|
||||
|
||||
bool PylontechCanReceiverClass::getBit(uint8_t value, uint8_t bit)
|
||||
bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit)
|
||||
{
|
||||
return (value & (1 << bit)) >> bit;
|
||||
}
|
||||
|
||||
#ifdef PYLONTECH_DUMMY
|
||||
void PylontechCanReceiver::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<float>((lastUpdate + offset) % 10) / 10);
|
||||
};
|
||||
|
||||
_stats->setManufacturer("Pylontech US3000C");
|
||||
_stats->setSoC(42);
|
||||
_stats->_chargeVoltage = dummyFloat(50);
|
||||
_stats->_chargeCurrentLimitation = dummyFloat(33);
|
||||
_stats->_dischargeCurrentLimitation = dummyFloat(12);
|
||||
_stats->_stateOfHealth = 99;
|
||||
_stats->_voltage = 48.67;
|
||||
_stats->_current = dummyFloat(-1);
|
||||
_stats->_temperature = dummyFloat(20);
|
||||
|
||||
_stats->_chargeEnabled = true;
|
||||
_stats->_dischargeEnabled = true;
|
||||
_stats->_chargeImmediately = false;
|
||||
|
||||
_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
|
||||
|
||||
@ -43,8 +43,7 @@ void WebApiClass::init()
|
||||
_webApiVedirect.init(&_server);
|
||||
_webApiWsHuaweiLive.init(&_server);
|
||||
_webApiHuaweiClass.init(&_server);
|
||||
_webApiWsPylontechLive.init(&_server);
|
||||
_webApiPylontechClass.init(&_server);
|
||||
_webApiWsBatteryLive.init(&_server);
|
||||
|
||||
_server.begin();
|
||||
}
|
||||
@ -76,8 +75,7 @@ void WebApiClass::loop()
|
||||
_webApiVedirect.loop();
|
||||
_webApiWsHuaweiLive.loop();
|
||||
_webApiHuaweiClass.loop();
|
||||
_webApiWsPylontechLive.loop();
|
||||
_webApiPylontechClass.loop();
|
||||
_webApiWsBatteryLive.loop();
|
||||
}
|
||||
|
||||
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Pylontech.h"
|
||||
#include "Battery.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiPylontechClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/battery/livedata", HTTP_GET, std::bind(&WebApiPylontechClass::onStatus, this, _1));
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::getJsonData(JsonObject& root) {
|
||||
|
||||
root["data_age"] = (millis() - Battery.lastUpdate) / 1000;
|
||||
|
||||
root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ;
|
||||
root[F("chargeVoltage")]["u"] = "V";
|
||||
root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ;
|
||||
root[F("chargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ;
|
||||
root[F("dischargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ;
|
||||
root[F("stateOfCharge")]["u"] = "%";
|
||||
root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ;
|
||||
root[F("stateOfHealth")]["u"] = "%";
|
||||
root[F("voltage")]["v"] = Battery.voltage;
|
||||
root[F("voltage")]["u"] = "V";
|
||||
root[F("current")]["v"] = Battery.current ;
|
||||
root[F("current")]["u"] = "A";
|
||||
root[F("temperature")]["v"] = Battery.temperature ;
|
||||
root[F("temperature")]["u"] = "°C";
|
||||
|
||||
// Alarms
|
||||
root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ;
|
||||
root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ;
|
||||
root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ;
|
||||
root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ;
|
||||
root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ;
|
||||
root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ;
|
||||
root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ;
|
||||
|
||||
// Warnings
|
||||
root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ;
|
||||
root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ;
|
||||
root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ;
|
||||
root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ;
|
||||
root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ;
|
||||
root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ;
|
||||
root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ;
|
||||
|
||||
// Misc
|
||||
root[F("manufacturer")] = Battery.manufacturer ;
|
||||
root[F("chargeEnabled")] = Battery.chargeEnabled ;
|
||||
root[F("dischargeEnabled")] = Battery.dischargeEnabled ;
|
||||
root[F("chargeImmediately")] = Battery.chargeImmediately ;
|
||||
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
@ -39,6 +39,10 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("enabled")] = config.Battery_Enabled;
|
||||
root[F("verbose_logging")] = config.Battery_VerboseLogging;
|
||||
root[F("provider")] = config.Battery_Provider;
|
||||
root[F("jkbms_interface")] = config.Battery_JkBmsInterface;
|
||||
root[F("jkbms_polling_interval")] = config.Battery_JkBmsPollingInterval;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -88,7 +92,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("enabled"))) {
|
||||
if (!root.containsKey(F("enabled")) || !root.containsKey(F("provider"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
@ -98,6 +102,10 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Battery_Enabled = root[F("enabled")].as<bool>();
|
||||
config.Battery_VerboseLogging = root[F("verbose_logging")].as<bool>();
|
||||
config.Battery_Provider = root[F("provider")].as<uint8_t>();
|
||||
config.Battery_JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>();
|
||||
config.Battery_JkBmsPollingInterval = root[F("jkbms_polling_interval")].as<uint8_t>();
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
@ -107,9 +115,5 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
if (config.Battery_Enabled) {
|
||||
PylontechCanReceiver.enable();
|
||||
} else {
|
||||
PylontechCanReceiver.disable();
|
||||
}
|
||||
Battery.init();
|
||||
}
|
||||
|
||||
@ -89,7 +89,9 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
|
||||
JsonObject batteryPinObj = curPin.createNestedObject("battery");
|
||||
batteryPinObj[F("rx")] = pin.battery_rx;
|
||||
batteryPinObj[F("rxen")] = pin.battery_rxen;
|
||||
batteryPinObj[F("tx")] = pin.battery_tx;
|
||||
batteryPinObj[F("txen")] = pin.battery_txen;
|
||||
|
||||
JsonObject huaweiPinObj = curPin.createNestedObject("huawei");
|
||||
huaweiPinObj[F("miso")] = pin.huawei_miso;
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_Pylontech.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsPylontechLiveClass::WebApiWsPylontechLiveClass()
|
||||
: _ws("/batterylivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
_server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsPylontechLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsPylontechLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::loop()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
if (millis() - _lastWsCleanup > 1000) {
|
||||
_ws.cleanupClients();
|
||||
_lastWsCleanup = millis();
|
||||
}
|
||||
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - _lastUpdateCheck < 1000) {
|
||||
return;
|
||||
}
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
String buffer;
|
||||
// free JsonDocument as soon as possible
|
||||
{
|
||||
DynamicJsonDocument root(1024);
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
serializeJson(root, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
{
|
||||
root["data_age"] = (millis() - Battery.lastUpdate) / 1000;
|
||||
|
||||
root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ;
|
||||
root[F("chargeVoltage")]["u"] = "V";
|
||||
root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ;
|
||||
root[F("chargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ;
|
||||
root[F("dischargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ;
|
||||
root[F("stateOfCharge")]["u"] = "%";
|
||||
root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ;
|
||||
root[F("stateOfHealth")]["u"] = "%";
|
||||
root[F("voltage")]["v"] = Battery.voltage;
|
||||
root[F("voltage")]["u"] = "V";
|
||||
root[F("current")]["v"] = Battery.current ;
|
||||
root[F("current")]["u"] = "A";
|
||||
root[F("temperature")]["v"] = Battery.temperature ;
|
||||
root[F("temperature")]["u"] = "°C";
|
||||
|
||||
// Alarms
|
||||
root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ;
|
||||
root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ;
|
||||
root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ;
|
||||
root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ;
|
||||
root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ;
|
||||
root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ;
|
||||
root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ;
|
||||
|
||||
// Warnings
|
||||
root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ;
|
||||
root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ;
|
||||
root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ;
|
||||
root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ;
|
||||
root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ;
|
||||
root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ;
|
||||
root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ;
|
||||
|
||||
// Misc
|
||||
root[F("manufacturer")] = Battery.manufacturer ;
|
||||
root[F("chargeEnabled")] = Battery.chargeEnabled ;
|
||||
root[F("dischargeEnabled")] = Battery.dischargeEnabled ;
|
||||
root[F("chargeImmediately")] = Battery.chargeImmediately ;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
|
||||
JsonVariant root = response->getRoot().as<JsonVariant>();
|
||||
generateJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
105
src/WebApi_ws_battery.cpp
Normal file
105
src/WebApi_ws_battery.cpp
Normal file
@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_battery.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass()
|
||||
: _ws("/batterylivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
_server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsBatteryLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::loop()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
if (millis() - _lastWsCleanup > 1000) {
|
||||
_ws.cleanupClients();
|
||||
_lastWsCleanup = millis();
|
||||
}
|
||||
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Battery.getStats()->updateAvailable(_lastUpdateCheck)) { return; }
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
String buffer;
|
||||
// free JsonDocument as soon as possible
|
||||
{
|
||||
DynamicJsonDocument root(_responseSize);
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
serializeJson(root, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
{
|
||||
Battery.getStats()->getLiveViewData(root);
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id());
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize);
|
||||
JsonVariant root = response->getRoot().as<JsonVariant>();
|
||||
generateJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
@ -196,7 +196,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
|
||||
JsonObject batteryObj = root.createNestedObject("battery");
|
||||
batteryObj[F("enabled")] = Configuration.get().Battery_Enabled;
|
||||
addTotalField(batteryObj, "soc", Battery.stateOfCharge, "%", 0);
|
||||
addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0);
|
||||
|
||||
JsonObject powerMeterObj = root.createNestedObject("power_meter");
|
||||
powerMeterObj[F("enabled")] = Configuration.get().PowerMeter_Enabled;
|
||||
|
||||
15
src/main.cpp
15
src/main.cpp
@ -9,7 +9,7 @@
|
||||
#include "Led_Single.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "VeDirectFrameHandler.h"
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "Battery.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MqttHandleDtu.h"
|
||||
#include "MqttHandleHass.h"
|
||||
@ -176,16 +176,6 @@ void setup()
|
||||
// Dynamic power limiter
|
||||
PowerLimiter.init();
|
||||
|
||||
// Initialize Pylontech Battery / CAN bus
|
||||
MessageOutput.println(F("Initialize Pylontech battery interface... "));
|
||||
if (PinMapping.isValidBatteryConfig()) {
|
||||
MessageOutput.printf("Pylontech Battery rx = %d, tx = %d\r\n", pin.battery_rx, pin.battery_tx);
|
||||
PylontechCanReceiver.init(pin.battery_rx, pin.battery_tx);
|
||||
MessageOutput.println(F("done"));
|
||||
} else {
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
}
|
||||
|
||||
// Initialize Huawei AC-charger PSU / CAN bus
|
||||
MessageOutput.println(F("Initialize Huawei AC charger interface... "));
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
@ -196,6 +186,7 @@ void setup()
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
}
|
||||
|
||||
Battery.init();
|
||||
}
|
||||
|
||||
void loop()
|
||||
@ -241,7 +232,7 @@ void loop()
|
||||
yield();
|
||||
MessageOutput.loop();
|
||||
yield();
|
||||
PylontechCanReceiver.loop();
|
||||
Battery.loop();
|
||||
yield();
|
||||
MqttHandlePylontechHass.loop();
|
||||
yield();
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-else>
|
||||
<div class="row gy-3">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': batteryData.data_age > 20,
|
||||
'text-bg-danger': batteryData.data_age >= 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
@ -29,7 +29,7 @@
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('battery.Status') }}</div>
|
||||
<div class="card-header text-bg-info">{{ $t('battery.Status') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
@ -40,45 +40,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.stateOfCharge') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.stateOfCharge.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.stateOfCharge.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.stateOfHealth') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.stateOfHealth.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.stateOfHealth.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.voltage') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.current') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.current.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.temperature') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.temperature.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.temperature.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeVoltage') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.chargeVoltage.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.chargeVoltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeCurrentLimitation') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.chargeCurrentLimitation.v }}</td>
|
||||
<td>{{ batteryData.chargeCurrentLimitation.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.dischargeCurrentLimitation') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.dischargeCurrentLimitation.v }}</td>
|
||||
<td>{{ batteryData.dischargeCurrentLimitation.u }}</td>
|
||||
<tr v-for="(prop, key) in batteryData.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>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="typeof prop === 'string'"></td>
|
||||
<td v-else>{{prop.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -86,164 +62,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('battery.warn_alarm') }}</div>
|
||||
<div class="card-body">
|
||||
<div class="card">
|
||||
<div :class="{'card-header': true, 'border-bottom-0': maxIssueValue === 0}">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
{{ $t('battery.issues') }}
|
||||
<div v-if="maxIssueValue === 0" class="badge text-bg-success">{{ $t('battery.noIssues') }}</div>
|
||||
<div v-else-if="maxIssueValue === 1" class="badge text-bg-warning text-dark">{{ $t('battery.warning') }}</div>
|
||||
<div v-else-if="maxIssueValue === 2" class="badge text-bg-danger">{{ $t('battery.alarm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" v-if="'issues' in batteryData">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th scope="col">{{ $t('battery.alarm') }}</th>
|
||||
<th scope="col">{{ $t('battery.warning') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueName') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueType') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.dischargeCurrent') }}</th>
|
||||
<tr v-for="(prop, key) in batteryData.issues" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.dischargeCurrent,
|
||||
'text-bg-success': !batteryData.alarms.dischargeCurrent
|
||||
'text-bg-warning text-dark': prop === 1,
|
||||
'text-bg-danger': prop === 2
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.dischargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-if="prop === 1">{{ $t('battery.warning') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.dischargeCurrent,
|
||||
'text-bg-success': !batteryData.warnings.dischargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.dischargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeCurrent') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.chargeCurrent,
|
||||
'text-bg-success': !batteryData.alarms.chargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.chargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.chargeCurrent,
|
||||
'text-bg-success': !batteryData.warnings.chargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.chargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.lowTemperature') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.lowTemperature,
|
||||
'text-bg-success': !batteryData.alarms.lowTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.lowTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.lowTemperature,
|
||||
'text-bg-success': !batteryData.warnings.lowTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.lowTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.highTemperature') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.highTemperature,
|
||||
'text-bg-success': !batteryData.alarms.highTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.highTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.highTemperature,
|
||||
'text-bg-success': !batteryData.warnings.highTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.highTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.lowVoltage') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.lowVoltage,
|
||||
'text-bg-success': !batteryData.alarms.lowVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.lowVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.lowVoltage,
|
||||
'text-bg-success': !batteryData.warnings.lowVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.lowVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.highVoltage') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.highVoltage,
|
||||
'text-bg-success': !batteryData.alarms.highVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.highVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.highVoltage,
|
||||
'text-bg-success': !batteryData.warnings.highVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.highVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.bmsInternal') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.bmsInternal,
|
||||
'text-bg-success': !batteryData.alarms.bmsInternal
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.bmsInternal">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.bmsInternal,
|
||||
'text-bg-success': !batteryData.warnings.bmsInternal
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.bmsInternal">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -256,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -295,7 +142,7 @@ export default defineComponent({
|
||||
console.log("Get initalData for Battery");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/battery/livedata", { headers: authHeader() })
|
||||
fetch("/api/batterylivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
@ -355,5 +202,10 @@ export default defineComponent({
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxIssueValue() {
|
||||
return ('issues' in this.batteryData)?Math.max(...Object.values(this.batteryData.issues)):0;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -28,6 +28,8 @@
|
||||
"Login": "Anmelden"
|
||||
},
|
||||
"base": {
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"VerboseLogging": "Ausführliche Protokollierung",
|
||||
"Loading": "Lade...",
|
||||
"Reload": "Aktualisieren"
|
||||
@ -559,9 +561,9 @@
|
||||
"LowerPowerLimit": "Unteres Leistungslimit",
|
||||
"UpperPowerLimit": "Oberes Leistungslimit",
|
||||
"PowerMeters": "Leistungsmesser",
|
||||
"BatterySocStartThreshold": "Akku SOC - Start",
|
||||
"BatterySocStopThreshold": "Akku SOC - Stop",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Akku SOC - Start solar passthrough",
|
||||
"BatterySocStartThreshold": "Akku SoC - Start",
|
||||
"BatterySocStopThreshold": "Akku SoC - Stop",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Akku SoC - Start solar passthrough",
|
||||
"BatterySocSolarPassthroughStartThresholdHint": "Wenn der Batterie SOC über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.",
|
||||
"VoltageStartThreshold": "DC Spannung - Start",
|
||||
"VoltageStopThreshold": "DC Spannung - Stop",
|
||||
@ -569,7 +571,7 @@
|
||||
"VoltageSolarPassthroughStopThreshold": "DC Spannung - Stop Solar-Passthrough",
|
||||
"VoltageSolarPassthroughStartThresholdHint": "Wenn der Batteriespannung über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist. Dieser Mode wird aktiv wenn das Start Spannungslimit überschritten wird und inaktiv wenn das Stop Spannungslimit unterschritten wird.",
|
||||
"VoltageLoadCorrectionFactor": "DC Spannung - Lastkorrekturfaktor",
|
||||
"BatterySocInfo": "<b>Hinweis:</b> Der Battery SOC (State of charge) -Wert kann nur benutzt werden wenn das Battery CAN Bus Interface aktiviert ist. Wenn die Batterie innerhalb der letzten Minute keine Werte geschickt hat, werden als Fallback-Option die Spannungseinstellungen verwendet.",
|
||||
"BatterySocInfo": "<b>Hinweis:</b> Die Akku SoC (State of Charge) Werte können nur benutzt werden, wenn die Batterie-Kommunikationsschnittstelle aktiviert ist. Wenn die Batterie innerhalb der letzten Minute keine Werte geschickt hat, werden als Fallback-Option die Spannungseinstellungen verwendet.",
|
||||
"InverterIsBehindPowerMeter": "Welchselrichter ist hinter Leistungsmesser",
|
||||
"Battery": "DC / Akku",
|
||||
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht normalerweise die Spannung etwas ein. Damit nicht vorzeitig der Wechelrichter ausgeschaltet wird sobald der \"Stop\"-Schwellenwert erreicht wird, wird der hier angegebene Korrekturfaktor mit einberechnet. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
|
||||
@ -580,8 +582,17 @@
|
||||
},
|
||||
"batteryadmin": {
|
||||
"BatterySettings": "Batterie Einstellungen",
|
||||
"BatteryConfiguration": "Batterie Konfiguration",
|
||||
"EnableBatteryCanBus": "Aktiviere Batterie CAN Bus Schnittstelle",
|
||||
"BatteryConfiguration": "Generelle Schnittstelleneinstellungen",
|
||||
"EnableBattery": "Aktiviere Schnittstelle",
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"Provider": "Datenanbieter",
|
||||
"ProviderPylontechCan": "Pylontech per CAN-Bus",
|
||||
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
|
||||
"JkBmsConfiguration": "JK BMS Einstellungen",
|
||||
"JkBmsInterface": "Schnittstellentyp",
|
||||
"JkBmsInterfaceUart": "TTL-UART an der MCU",
|
||||
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
|
||||
"PollingInterval": "Abfrageintervall",
|
||||
"Seconds": "@:dtuadmin.Seconds",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
@ -761,9 +772,11 @@
|
||||
"Seconds": "vor {val} Sekunden",
|
||||
"Status": "Status",
|
||||
"Property": "Eigenschaft",
|
||||
"yes": "@:base.Yes",
|
||||
"no": "@:base.No",
|
||||
"Value": "Wert",
|
||||
"Unit": "Einheit",
|
||||
"stateOfCharge": "Ladezustand (SoC)",
|
||||
"SoC": "Ladezustand (SoC)",
|
||||
"stateOfHealth": "Batteriezustand (SoH)",
|
||||
"voltage": "Spannung",
|
||||
"current": "Strom",
|
||||
@ -771,16 +784,27 @@
|
||||
"chargeVoltage": "Gewünschte Ladespannung (BMS)",
|
||||
"chargeCurrentLimitation": "Ladestromlimit",
|
||||
"dischargeCurrentLimitation": "Entladestromlimit",
|
||||
"warn_alarm": "Warnungen und Alarme",
|
||||
"ok": "OK",
|
||||
"chargeEnabled": "Laden ermöglicht",
|
||||
"dischargeEnabled": "Entladen ermöglicht",
|
||||
"chargeImmediately": "Sofortiges Laden angefordert",
|
||||
"issues": "Meldungen",
|
||||
"noIssues": "Keine Meldungen",
|
||||
"issueName": "Bezeichnung",
|
||||
"issueType": "Art",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warnung",
|
||||
"dischargeCurrent": "Entladestrom",
|
||||
"chargeCurrent": "Ladestrom",
|
||||
"lowTemperature": "Temperatur niedrig",
|
||||
"highTemperature": "Temperatur hoch",
|
||||
"lowVoltage": "Spannung niedrig",
|
||||
"highVoltage": "Spannung hoch",
|
||||
"highCurrentDischarge": "Hoher Entladestrom",
|
||||
"overCurrentDischarge": "Überstrom (Entladen)",
|
||||
"highCurrentCharge": "Hoher Ladestrom",
|
||||
"overCurrentCharge": "Überstrom (Laden)",
|
||||
"lowTemperature": "Geringe Temperatur",
|
||||
"underTemperature": "Untertemperatur",
|
||||
"highTemperature": "Hohe Temperatur",
|
||||
"overTemperature": "Übertemperatur",
|
||||
"lowVoltage": "Niedrige Spannung",
|
||||
"underVoltage": "Unterspannung",
|
||||
"highVoltage": "Hohe Spannung",
|
||||
"overVoltage": "Überspannung",
|
||||
"bmsInternal": "BMS intern"
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
"Login": "Login"
|
||||
},
|
||||
"base": {
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"VerboseLogging": "Verbose Logging",
|
||||
"Loading": "Loading...",
|
||||
"Reload": "Reload"
|
||||
@ -568,9 +570,9 @@
|
||||
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
|
||||
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (optional)",
|
||||
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (optional)",
|
||||
"BatterySocStartThreshold": "Battery SOC - Start threshold",
|
||||
"BatterySocStopThreshold": "Battery SOC - Stop threshold",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Battery SOC - Start threshold for full solar passthrough",
|
||||
"BatterySocStartThreshold": "Battery SoC - Start threshold",
|
||||
"BatterySocStopThreshold": "Battery SoC - Stop threshold",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough",
|
||||
"BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SoC is over this limit. Use this if you like to supply excess power to the grid when battery is full",
|
||||
"VoltageStartThreshold": "DC Voltage - Start threshold",
|
||||
"VoltageStopThreshold": "DC Voltage - Stop threshold",
|
||||
@ -578,7 +580,7 @@
|
||||
"VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough",
|
||||
"VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.",
|
||||
"VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SOC (State of charge) values can only be used when the Battery CAN Bus interface is enabled. If the battery has not reported any updates of SOC in the last minute, the voltage thresholds will be used as fallback.",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values can only be used if the battery communication interface is enabled. If the battery has not reported any SoC updates in the last minute, the voltage thresholds will be used as fallback.",
|
||||
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
|
||||
"Battery": "DC / Battery",
|
||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
|
||||
@ -589,8 +591,17 @@
|
||||
},
|
||||
"batteryadmin": {
|
||||
"BatterySettings": "Battery Settings",
|
||||
"BatteryConfiguration": "Battery Configuration",
|
||||
"EnableBatteryCanBus": "Enable Battery CAN Bus Interface",
|
||||
"BatteryConfiguration": "General Interface Settings",
|
||||
"EnableBattery": "Enable Interface",
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"Provider": "Data Provider",
|
||||
"ProviderPylontechCan": "Pylontech using CAN bus",
|
||||
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
||||
"JkBmsConfiguration": "JK BMS Settings",
|
||||
"JkBmsInterface": "Interface Type",
|
||||
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
||||
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
|
||||
"PollingInterval": "Polling Interval",
|
||||
"Seconds": "@:dtuadmin.Seconds",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
@ -766,31 +777,44 @@
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
"battery": {
|
||||
"battery": "battery",
|
||||
"battery": "Battery",
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
"Status": "Status",
|
||||
"Property": "Property",
|
||||
"yes": "@:base.Yes",
|
||||
"no": "@:base.No",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"stateOfCharge": "State of charge",
|
||||
"stateOfHealth": "State of health",
|
||||
"SoC": "State of Charge",
|
||||
"stateOfHealth": "State of Health",
|
||||
"voltage": "Voltage",
|
||||
"current": "Current",
|
||||
"temperature": "Temperature",
|
||||
"chargeVoltage": "Requested charge voltage",
|
||||
"chargeCurrentLimitation": "Charge current limit",
|
||||
"dischargeCurrentLimitation": "Discharge current limit",
|
||||
"warn_alarm": "Alarms and warnings",
|
||||
"ok": "OK",
|
||||
"chargeEnabled": "Charging possible",
|
||||
"dischargeEnabled": "Discharging possible",
|
||||
"chargeImmediately": "Immediate charging requested",
|
||||
"issues": "Issues",
|
||||
"noIssues": "No Issues",
|
||||
"issueName": "Name",
|
||||
"issueType": "Type",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warning",
|
||||
"dischargeCurrent": "Discharge current",
|
||||
"chargeCurrent": "Charge current",
|
||||
"highCurrentDischarge": "High current (discharge)",
|
||||
"overCurrentDischarge": "Overcurrent (discharge)",
|
||||
"highCurrentCharge": "High current (charge)",
|
||||
"overCurrentCharge": "Overcurrent (charge)",
|
||||
"lowTemperature": "Low temperature",
|
||||
"underTemperature": "Undertemperature",
|
||||
"highTemperature": "High temperature",
|
||||
"overTemperature": "Overtemperature",
|
||||
"lowVoltage": "Low voltage",
|
||||
"underVoltage": "Undervoltage",
|
||||
"highVoltage": "High voltage",
|
||||
"overVoltage": "Overvoltage",
|
||||
"bmsInternal": "BMS internal"
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
"Login": "Connexion"
|
||||
},
|
||||
"base": {
|
||||
"Yes": "Oui",
|
||||
"No": "Non",
|
||||
"VerboseLogging": "Journalisation Détaillée",
|
||||
"Loading": "Chargement...",
|
||||
"Reload": "Reload"
|
||||
@ -584,9 +586,9 @@
|
||||
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
|
||||
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (optional)",
|
||||
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (optional)",
|
||||
"BatterySocStartThreshold": "Battery SOC - Start threshold",
|
||||
"BatterySocStopThreshold": "Battery SOC - Stop threshold",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Battery SOC - Start threshold for full solar passthrough",
|
||||
"BatterySocStartThreshold": "Battery SoC - Start threshold",
|
||||
"BatterySocStopThreshold": "Battery SoC - Stop threshold",
|
||||
"BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough",
|
||||
"BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SOC is over this limit. Use this if you like to supply excess power to the grid when battery is full",
|
||||
"VoltageStartThreshold": "DC Voltage - Start threshold",
|
||||
"VoltageStopThreshold": "DC Voltage - Stop threshold",
|
||||
@ -594,7 +596,7 @@
|
||||
"VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough",
|
||||
"VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.",
|
||||
"VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SOC (State of charge) values can only be used when the Battery CAN Bus interface is enabled. If the battery has not reported any updates of SOC in the last minute, the voltage thresholds will be used as fallback.",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values can only be used if the battery communication interface is enabled. If the battery has not reported any SoC updates in the last minute, the voltage thresholds will be used as fallback.",
|
||||
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
|
||||
"Battery": "DC / Battery",
|
||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
|
||||
@ -720,9 +722,11 @@
|
||||
"Seconds": " {val} seconds",
|
||||
"Status": "Status",
|
||||
"Property": "Property",
|
||||
"yes": "@:base.Yes",
|
||||
"no": "@:base.No",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"stateOfCharge": "State of charge",
|
||||
"SoC": "State of charge",
|
||||
"stateOfHealth": "State of health",
|
||||
"voltage": "Voltage",
|
||||
"current": "Current",
|
||||
@ -730,16 +734,24 @@
|
||||
"chargeVoltage": "Requested charge voltage",
|
||||
"chargeCurrentLimitation": "Charge current limit",
|
||||
"dischargeCurrentLimitation": "Discharge current limit",
|
||||
"warn_alarm": "Alarms and warnings",
|
||||
"ok": "OK",
|
||||
"issues": "Issues",
|
||||
"noIssues": "No Issues",
|
||||
"issueName": "Name",
|
||||
"issueType": "Type",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warning",
|
||||
"dischargeCurrent": "Discharge current",
|
||||
"chargeCurrent": "Charge current",
|
||||
"highCurrentDischarge": "High current (discharge)",
|
||||
"overCurrentDischarge": "Overcurrent (discharge)",
|
||||
"highCurrentCharge": "High current (charge)",
|
||||
"overCurrentCharge": "Overcurrent (charge)",
|
||||
"lowTemperature": "Low temperature",
|
||||
"underTemperature": "Undertemperature",
|
||||
"highTemperature": "High temperature",
|
||||
"overTemperature": "Overtemperature",
|
||||
"lowVoltage": "Low voltage",
|
||||
"underVoltage": "Undervoltage",
|
||||
"highVoltage": "High voltage",
|
||||
"overVoltage": "Overvoltage",
|
||||
"bmsInternal": "BMS internal"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
export interface BatteryConfig {
|
||||
enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
provider: number;
|
||||
jkbms_interface: number;
|
||||
jkbms_polling_interval: number;
|
||||
}
|
||||
|
||||
@ -1,32 +1,8 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
interface BatteryFlags {
|
||||
dischargeCurrent: boolean;
|
||||
chargeCurrent: boolean;
|
||||
lowTemperature: boolean;
|
||||
highTemperature: boolean;
|
||||
lowVoltage: boolean;
|
||||
highVoltage: boolean;
|
||||
bmsInternal: boolean;
|
||||
}
|
||||
|
||||
|
||||
// Battery
|
||||
export interface Battery {
|
||||
data_age: 0;
|
||||
chargeVoltage: ValueObject;
|
||||
chargeCurrentLimitation: ValueObject;
|
||||
dischargeCurrentLimitation: ValueObject;
|
||||
stateOfCharge: ValueObject;
|
||||
stateOfChargeLastUpdate: ValueObject;
|
||||
stateOfHealth: ValueObject;
|
||||
voltage: ValueObject;
|
||||
current: ValueObject;
|
||||
temperature: ValueObject;
|
||||
warnings: BatteryFlags;
|
||||
alarms: BatteryFlags;
|
||||
manufacturer: string;
|
||||
chargeEnabled: boolean;
|
||||
dischargeEnabled: boolean;
|
||||
chargeImmediately: boolean;
|
||||
data_age: number;
|
||||
values: (ValueObject | string)[];
|
||||
issues: number[];
|
||||
}
|
||||
@ -6,9 +6,47 @@
|
||||
|
||||
<form @submit="saveBatteryConfig">
|
||||
<CardElement :text="$t('batteryadmin.BatteryConfiguration')" textVariant="text-bg-primary">
|
||||
<InputElement :label="$t('batteryadmin.EnableBatteryCanBus')"
|
||||
<InputElement :label="$t('batteryadmin.EnableBattery')"
|
||||
v-model="batteryConfigList.enabled"
|
||||
type="checkbox" wide/>
|
||||
type="checkbox" />
|
||||
|
||||
<InputElement v-show="batteryConfigList.enabled"
|
||||
:label="$t('batteryadmin.VerboseLogging')"
|
||||
v-model="batteryConfigList.verbose_logging"
|
||||
type="checkbox"/>
|
||||
|
||||
<div class="row mb-3" v-show="batteryConfigList.enabled">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{{ $t('batteryadmin.Provider') }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="batteryConfigList.provider">
|
||||
<option v-for="provider in providerTypeList" :key="provider.key" :value="provider.key">
|
||||
{{ $t(`batteryadmin.Provider` + provider.value) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<CardElement v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
|
||||
:text="$t('batteryadmin.JkBmsConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{{ $t('batteryadmin.JkBmsInterface') }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="batteryConfigList.jkbms_interface">
|
||||
<option v-for="jkBmsInterface in jkBmsInterfaceTypeList" :key="jkBmsInterface.key" :value="jkBmsInterface.key">
|
||||
{{ $t(`batteryadmin.JkBmsInterface` + jkBmsInterface.value) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.PollingInterval')"
|
||||
v-model="batteryConfigList.jkbms_polling_interval"
|
||||
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/>
|
||||
</CardElement>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-3">{{ $t('batteryadmin.Save') }}</button>
|
||||
@ -39,6 +77,14 @@ export default defineComponent({
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
providerTypeList: [
|
||||
{ key: 0, value: 'PylontechCan' },
|
||||
{ key: 1, value: 'JkBmsSerial' },
|
||||
],
|
||||
jkBmsInterfaceTypeList: [
|
||||
{ key: 0, value: 'Uart' },
|
||||
{ key: 1, value: 'Transceiver' },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user