Merge branch 'development'

This commit is contained in:
helgeerbe 2024-02-09 20:35:46 +01:00
commit 0c09c39db4
21 changed files with 357 additions and 295 deletions

View File

@ -46,7 +46,7 @@ This project is still under development and adds following features:
[Full documentation of OpenDTU-onBattery extensions can be found at the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki).
For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [wiki](https://github.com/tbnobody/OpenDTU/wiki).
For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [documentation](https://tbnobody.github.io/OpenDTU-docs/).
Please note that openDTU-onBattery may change significantly during its development.
Bug reports, comments, feature requests and fixes are most welcome!

View File

@ -1,20 +1,3 @@
# Display integration
OpenDTU currently supports 3 types of displays (SSD1306, SH1106 and PCD8544). Currently only displays with a resolution of 128x64 pixel are supported. To activate a display you have to specify it's type and pin assignment either in the `platformio_override.ini` or in a device profile. Due to the fact that device profiles work with the pre-compiled binary the following documentation will only cover the device profile method.
You can either create your own device profile as described [here](DeviceProfiles.md) or use some pre-defined. The pre-defined profiles can be found [here](DeviceProfiles/). You can simply open the json file with a text editor of your choice to view/edit the pin assignment.
## Uploading Device Profiles
Use the "Config Management" site to upload (Restore) the json file. Make sure to choose "Pin Mapping (pin_mapping.json)" in the combo box. After you click on restore the ESP will restart. At this point, the profile is not yet active. Please read the next chapter.
![Config Management](screenshots/14_ConfigManagement.png)
## Selecting a Device Profile
After you uploaded the device profile you can select the profile in the "Device Manager" view. After a click on "Save" the ESP will be restarted and the pin assignment is active. At this point the display should already show something. Please see the next chapter for display settings.
![Device Manager](screenshots/20_DeviceManager_Pin.png)
## Display Settings
Display settings can also be found in the "Device Manager".
![Device Manager Display](screenshots/21_DeviceManager_Display.png)
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/hardware/display/>

View File

@ -1,21 +1,3 @@
# Upgrade Partition
To be able to install further updates you have to update the partition table of the ESP32. Doing so will **erase** all configuration data. Over The Air update using the web interface is **NOT** possible!
**So make sure you export a backup of your configuration files before continuing.**
There are several possibilities to update the partition table:
- Using Visual Studio Code or PlatformIO CLI
If you have already used Visual Studio Code or the `platformio` command you can use it again to install the latest version. The partition table is upgraded automatically.
- Any kind of flash interface
If you like to use any kind of flash interface like `esptool.py`, Espressif Flash Download Tool, ESP_Flasher or esptool-js you have to make sure to upload the provided .factory.bin file. It is important to enter the correct target address.
| Address | File |
| ---------| ---------------------- |
| 0x0 | opendtu-*.factory.bin |
After upgrading the ESP32 will open the intergrated access point (AP) again. Just connect to it using the default password ("openDTU42"). If you are connected, just visit <http://192.168.4.1> and enter the "Configuration Management". Recover the previously backuped config files.
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/howto/upgrade_partition/>

View File

@ -29,7 +29,6 @@ class BatteryClass {
Task _loopTask;
uint32_t _lastMqttPublish = 0;
mutable std::mutex _mutex;
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
};

View File

@ -23,15 +23,24 @@ class BatteryStats {
// convert stats to JSON for web application live view
virtual void getLiveViewData(JsonVariant& root) const;
virtual void mqttPublish() const;
void mqttLoop();
// the interval at which all battery datums will be re-published, even
// if they did not change. used to calculate Home Assistent expiration.
virtual uint32_t getMqttFullPublishIntervalMs() const;
bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
protected:
virtual void mqttPublish() const;
String _manufacturer = "unknown";
uint8_t _SoC = 0;
uint32_t _lastUpdateSoC = 0;
uint32_t _lastUpdate = 0;
private:
uint32_t _lastMqttPublish = 0;
};
class PylontechBatteryStats : public BatteryStats {
@ -89,6 +98,8 @@ class JkBmsBatteryStats : public BatteryStats {
void mqttPublish() const final;
uint32_t getMqttFullPublishIntervalMs() const final { return 60 * 1000; }
void updateFrom(JkBms::DataPointContainer const& dp);
private:

View File

@ -20,6 +20,7 @@ enum DisplayType_t {
SSD1306,
SH1106,
SSD1309,
ST7567_GM12864I_59N,
DisplayType_Max,
};

View File

@ -4,11 +4,10 @@
#include <ArduinoJson.h>
#include <TaskSchedulerDeclarations.h>
class MqttHandlePylontechHassClass {
class MqttHandleBatteryHassClass {
public:
void init(Scheduler& scheduler);
void publishConfig();
void forceUpdate();
void forceUpdate() { _doPublish = true; }
private:
void loop();
@ -19,9 +18,8 @@ private:
Task _loopTask;
bool _wasConnected = false;
bool _updateForced = false;
bool _doPublish = true;
String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber
};
extern MqttHandlePylontechHassClass MqttHandlePylontechHass;
extern MqttHandleBatteryHassClass MqttHandleBatteryHass;

View File

@ -301,7 +301,20 @@ uint32_t VeDirectFrameHandler::getLastUpdate() const
*/
frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{
static constexpr frozen::map<uint16_t, frozen::string, 77> values = {
/**
* this map is rendered from [1], which is more recent than [2]. Phoenix
* inverters are not included in the map. unfortunately, the documents do
* not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110,
* and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in
* [1] but rev2 in [2].
*
* [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
* [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf
*/
static constexpr frozen::map<uint16_t, frozen::string, 105> values = {
{ 0x0203, "BMV-700" },
{ 0x0204, "BMV-702" },
{ 0x0205, "BMV-700H" },
{ 0x0300, "BlueSolar MPPT 70|15" },
{ 0xA040, "BlueSolar MPPT 75|50" },
{ 0xA041, "BlueSolar MPPT 150|35" },
@ -309,8 +322,9 @@ frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ 0xA043, "BlueSolar MPPT 100|15" },
{ 0xA044, "BlueSolar MPPT 100|30" },
{ 0xA045, "BlueSolar MPPT 100|50" },
{ 0xA046, "BlueSolar MPPT 100|70" },
{ 0xA046, "BlueSolar MPPT 150|70" },
{ 0xA047, "BlueSolar MPPT 150|100" },
{ 0xA048, "BlueSolar MPPT 75|50 rev2" },
{ 0xA049, "BlueSolar MPPT 100|50 rev2" },
{ 0xA04A, "BlueSolar MPPT 100|30 rev2" },
{ 0xA04B, "BlueSolar MPPT 150|35 rev2" },
@ -327,7 +341,7 @@ frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ 0xA056, "SmartSolar MPPT 100|30" },
{ 0xA057, "SmartSolar MPPT 100|50" },
{ 0xA058, "SmartSolar MPPT 150|35" },
{ 0xA059, "SmartSolar MPPT 150|10 rev2" },
{ 0xA059, "SmartSolar MPPT 150|100 rev2" },
{ 0xA05A, "SmartSolar MPPT 150|85 rev2" },
{ 0xA05B, "SmartSolar MPPT 250|70" },
{ 0xA05C, "SmartSolar MPPT 250|85" },
@ -352,6 +366,20 @@ frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ 0xA06F, "BlueSolar MPPT 150|45 rev2" },
{ 0xA070, "BlueSolar MPPT 150|60 rev2" },
{ 0xA071, "BlueSolar MPPT 150|70 rev2" },
{ 0xA072, "BlueSolar MPPT 150|45 rev3" },
{ 0xA073, "SmartSolar MPPT 150|45 rev3" },
{ 0xA074, "SmartSolar MPPT 75|10 rev2" },
{ 0xA075, "SmartSolar MPPT 75|15 rev2" },
{ 0xA076, "BlueSolar MPPT 100|30 rev3" },
{ 0xA077, "BlueSolar MPPT 100|50 rev3" },
{ 0xA078, "BlueSolar MPPT 150|35 rev3" },
{ 0xA079, "BlueSolar MPPT 75|10 rev2" },
{ 0xA07A, "BlueSolar MPPT 75|15 rev2" },
{ 0xA07B, "BlueSolar MPPT 100|15 rev2" },
{ 0xA07C, "BlueSolar MPPT 75|10 rev3" },
{ 0xA07D, "BlueSolar MPPT 75|15 rev3" },
{ 0xA07E, "SmartSolar MPPT 100|30 12V" },
{ 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" },
{ 0xA102, "SmartSolar MPPT VE.Can 150|70" },
{ 0xA103, "SmartSolar MPPT VE.Can 150|45" },
{ 0xA104, "SmartSolar MPPT VE.Can 150|60" },
@ -359,7 +387,7 @@ frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ 0xA106, "SmartSolar MPPT VE.Can 150|100" },
{ 0xA107, "SmartSolar MPPT VE.Can 250|45" },
{ 0xA108, "SmartSolar MPPT VE.Can 250|60" },
{ 0xA109, "SmartSolar MPPT VE.Can 250|80" },
{ 0xA109, "SmartSolar MPPT VE.Can 250|70" },
{ 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
{ 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
{ 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
@ -367,18 +395,28 @@ frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
{ 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
{ 0xA110, "SmartSolar MPPT RS 450|100" },
{ 0xA111, "SmartSolar MPPT RS 450|200" },
{ 0xA112, "BlueSolar MPPT VE.Can 250|70" },
{ 0xA113, "BlueSolar MPPT VE.Can 250|100" },
{ 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
{ 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
{ 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
{ 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" },
{ 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" },
{ 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" },
{ 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" },
{ 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" },
{ 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" },
{ 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" },
{ 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" },
{ 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" },
{ 0xA381, "BMV-712 Smart" },
{ 0xA382, "BMV-710H Smart" },
{ 0xA383, "BMV-712 Smart Rev2" },
{ 0xA389, "SmartShunt 500A/50mV" },
{ 0xA38A, "SmartShunt 1000A/50mV" },
{ 0xA38B, "SmartShunt 2000A/50mV" },
{ 0xA3F0, "SmartShunt 2000A/50mV" }
{ 0xA3F0, "Smart BuckBoost 12V/12V-50A" },
};
return getAsString(values, PID);

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Battery.h"
#include "MessageOutput.h"
#include "MqttSettings.h"
#include "PylontechCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
@ -76,14 +75,5 @@ void BatteryClass::loop()
_upProvider->loop();
CONFIG_T& config = Configuration.get();
if (!MqttSettings.getConnected()
|| (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) {
return;
}
_upProvider->getStats()->mqttPublish();
_lastMqttPublish = millis();
_upProvider->getStats()->mqttLoop();
}

View File

@ -5,6 +5,7 @@
#include "Configuration.h"
#include "MqttSettings.h"
#include "JkBmsDataPoints.h"
#include "MqttSettings.h"
template<typename T>
static void addLiveViewInSection(JsonVariant& root,
@ -187,6 +188,31 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
}
}
void BatteryStats::mqttLoop()
{
auto& config = Configuration.get();
if (!MqttSettings.getConnected()
|| (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) {
return;
}
mqttPublish();
_lastMqttPublish = millis();
}
uint32_t BatteryStats::getMqttFullPublishIntervalMs() const
{
auto& config = Configuration.get();
// this is the default interval, see mqttLoop(). mqttPublish()
// implementations in derived classes may choose to publish some values
// with a lower frequency and hence implement this method with a different
// return value.
return config.Mqtt.PublishInterval * 1000;
}
void BatteryStats::mqttPublish() const
{
MqttSettings.publish(F("battery/manufacturer"), _manufacturer);
@ -236,11 +262,10 @@ void JkBmsBatteryStats::mqttPublish() const
Label::BatterySoCPercent // already published by base class
};
CONFIG_T& config = Configuration.get();
// publish all topics every minute, unless the retain flag is enabled
bool fullPublish = _lastFullMqttPublish + 60 * 1000 < millis();
fullPublish &= !config.Mqtt.Retain;
// regularly publish all topics regardless of whether or not their value changed
bool neverFullyPublished = _lastFullMqttPublish == 0;
bool intervalElapsed = _lastFullMqttPublish + getMqttFullPublishIntervalMs() < millis();
bool fullPublish = neverFullyPublished || intervalElapsed;
for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
// skip data points that did not change since last published

View File

@ -13,6 +13,7 @@ std::map<DisplayType_t, std::function<U8G2*(uint8_t, uint8_t, uint8_t, uint8_t)>
{ DisplayType_t::SSD1306, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } },
{ DisplayType_t::SH1106, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } },
{ DisplayType_t::SSD1309, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(U8G2_R0, reset, clock, data); } },
{ DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } },
};
// Language defintion, respect order in languages[] and translation lists
@ -50,6 +51,9 @@ void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, c
if (isValidDisplay()) {
auto constructor = display_types[_display_type];
_display = constructor(reset, clk, data, cs);
if (_display_type == DisplayType_t::ST7567_GM12864I_59N) {
_display->setI2CAddress(0x3F << 1);
}
_display->begin();
setContrast(DISPLAY_CONTRAST);
setStatus(true);

View File

@ -109,6 +109,7 @@ void InverterSettingsClass::init(Scheduler& scheduler)
void InverterSettingsClass::settingsLoop()
{
const CONFIG_T& config = Configuration.get();
const bool isDayPeriod = SunPosition.isDayPeriod();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
auto const& inv_cfg = config.Inverter[i];
@ -120,8 +121,8 @@ void InverterSettingsClass::settingsLoop()
continue;
}
inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night));
inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night));
inv->setEnablePolling(inv_cfg.Poll_Enable && (isDayPeriod || inv_cfg.Poll_Enable_Night));
inv->setEnableCommands(inv_cfg.Command_Enable && (isDayPeriod || inv_cfg.Command_Enable_Night));
}
}

View File

@ -0,0 +1,237 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "PylontechCanReceiver.h"
#include "Battery.h"
#include "MqttHandleBatteryHass.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "Utils.h"
MqttHandleBatteryHassClass MqttHandleBatteryHass;
void MqttHandleBatteryHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandleBatteryHassClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
void MqttHandleBatteryHassClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
if (!config.Mqtt.Hass.Enabled) { return; }
// TODO(schlimmchen): this cannot make sure that transient
// connection problems are actually always noticed.
if (!MqttSettings.getConnected()) {
_doPublish = true;
return;
}
// only publish HA config once when (re-)connecting
// to the MQTT broker or on config changes.
if (!_doPublish) { return; }
// the MQTT battery provider does not re-publish the SoC under a different
// known topic. we don't know the manufacture either. HASS auto-discovery
// for that provider makes no sense.
if (config.Battery.Provider != 2) {
publishSensor("Manufacturer", "mdi:factory", "manufacturer");
publishSensor("Data Age", "mdi:timer-sand", "dataAge", "duration", "measurement", "s");
publishSensor("State of Charge (SoC)", "mdi:battery-medium", "stateOfCharge", "battery", "measurement", "%");
}
switch (config.Battery.Provider) {
case 0: // Pylontech Battery
publishSensor("Battery voltage", NULL, "voltage", "voltage", "measurement", "V");
publishSensor("Battery current", NULL, "current", "current", "measurement", "A");
publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C");
publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%");
publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V");
publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A");
publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A");
publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0");
publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");
publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0");
publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0");
publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0");
publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0");
publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0");
publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0");
publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0");
publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0");
publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0");
publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0");
publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0");
publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0");
publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");
publishBinarySensor("Charge immediately", "mdi:alert", "charging/chargeImmediately", "1", "0");
break;
case 1: // JK BMS
// caption icon topic dev. class state class unit
publishSensor("Voltage", "mdi:battery-charging", "BatteryVoltageMilliVolt", "voltage", "measurement", "mV");
publishSensor("Current", "mdi:current-dc", "BatteryCurrentMilliAmps", "current", "measurement", "mA");
publishSensor("BMS Temperature", "mdi:thermometer", "BmsTempCelsius", "temperature", "measurement", "°C");
publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV");
publishSensor("Charge Cycles", "mdi:counter", "BatteryCycles");
publishSensor("Cycle Capacity", "mdi:battery-sync", "BatteryCycleCapacity");
publishBinarySensor("Charging Possible", "mdi:battery-arrow-up", "status/ChargingActive", "1", "0");
publishBinarySensor("Discharging Possible", "mdi:battery-arrow-down", "status/DischargingActive", "1", "0");
publishBinarySensor("Balancing Active", "mdi:scale-balance", "status/BalancingActive", "1", "0");
#define PBS(a, b, c) publishBinarySensor("Alarm: " a, "mdi:" b, "alarms/" c, "1", "0")
PBS("Low Capacity", "battery-alert-variant-outline", "LowCapacity");
PBS("BMS Overtemperature", "thermometer-alert", "BmsOvertemperature");
PBS("Charging Overvoltage", "fuse-alert", "ChargingOvervoltage");
PBS("Discharge Undervoltage", "fuse-alert", "DischargeUndervoltage");
PBS("Battery Overtemperature", "thermometer-alert", "BatteryOvertemperature");
PBS("Charging Overcurrent", "fuse-alert", "ChargingOvercurrent");
PBS("Discharging Overcurrent", "fuse-alert", "DischargeOvercurrent");
PBS("Cell Voltage Difference", "battery-alert", "CellVoltageDifference");
PBS("Battery Box Overtemperature", "thermometer-alert", "BatteryBoxOvertemperature");
PBS("Battery Undertemperature", "thermometer-alert", "BatteryUndertemperature");
PBS("Cell Overvoltage", "battery-alert", "CellOvervoltage");
PBS("Cell Undervoltage", "battery-alert", "CellUndervoltage");
#undef PBS
break;
case 2: // SoC from MQTT
break;
case 3: // Victron SmartShunt
break;
}
_doPublish = false;
}
void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption;
root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId;
if (icon != NULL) {
root["icon"] = icon;
}
if (unitOfMeasurement != NULL) {
root["unit_of_meas"] = unitOfMeasurement;
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() * 3;
}
if (deviceClass != NULL) {
root["dev_cla"] = deviceClass;
}
if (stateClass != NULL) {
root["stat_cla"] = stateClass;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.replace(":", "");
sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (icon != NULL) {
root["icon"] = icon;
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleBatteryHassClass::createDeviceInfo(JsonObject& object)
{
object["name"] = "Battery(" + serial + ")";
auto& config = Configuration.get();
if (config.Battery.Provider == 1) {
object["name"] = "JK BMS (" + Battery.getStats()->getManufacturer() + ")";
}
object["ids"] = serial;
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object["mf"] = "OpenDTU";
object["mdl"] = Battery.getStats()->getManufacturer();
object["sw"] = AUTO_GIT_HASH;
}
void MqttHandleBatteryHassClass::publish(const String& subtopic, const String& payload)
{
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
}

View File

@ -1,209 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "PylontechCanReceiver.h"
#include "Battery.h"
#include "MqttHandlePylontechHass.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "MessageOutput.h"
#include "Utils.h"
MqttHandlePylontechHassClass MqttHandlePylontechHass;
void MqttHandlePylontechHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandlePylontechHassClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
void MqttHandlePylontechHassClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!config.Battery.Enabled) {
return;
}
if (_updateForced) {
publishConfig();
_updateForced = false;
}
if (MqttSettings.getConnected() && !_wasConnected) {
// Connection established
_wasConnected = true;
publishConfig();
} else if (!MqttSettings.getConnected() && _wasConnected) {
// Connection lost
_wasConnected = false;
}
}
void MqttHandlePylontechHassClass::forceUpdate()
{
_updateForced = true;
}
void MqttHandlePylontechHassClass::publishConfig()
{
CONFIG_T& config = Configuration.get();
if ((!config.Mqtt.Hass.Enabled) || (!config.Battery.Enabled)) {
return;
}
if (!MqttSettings.getConnected()) {
return;
}
// device info
publishSensor("Manufacturer", "mdi:factory", "manufacturer");
// battery info
publishSensor("Battery voltage", NULL, "voltage", "voltage", "measurement", "V");
publishSensor("Battery current", NULL, "current", "current", "measurement", "A");
publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C");
publishSensor("State of Charge (SOC)", NULL, "stateOfCharge", "battery", "measurement", "%");
publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%");
publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V");
publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A");
publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A");
publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0");
publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");
publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0");
publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0");
publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0");
publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0");
publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0");
publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0");
publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0");
publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0");
publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0");
publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0");
publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0");
publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0");
publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");
publishBinarySensor("Charge immediately", "mdi:alert", "charging/chargeImmediately", "1", "0");
yield();
}
void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption;
root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId;
if (icon != NULL) {
root["icon"] = icon;
}
if (unitOfMeasurement != NULL) {
root["unit_of_meas"] = unitOfMeasurement;
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
}
if (deviceClass != NULL) {
root["dev_cla"] = deviceClass;
}
if (stateClass != NULL) {
root["stat_cla"] = stateClass;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (icon != NULL) {
root["icon"] = icon;
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandlePylontechHassClass::createDeviceInfo(JsonObject& object)
{
object["name"] = "Battery(" + serial + ")";
object["ids"] = serial;
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object["mf"] = "OpenDTU";
object["mdl"] = Battery.getStats()->getManufacturer();
object["sw"] = AUTO_GIT_HASH;
}
void MqttHandlePylontechHassClass::publish(const String& subtopic, const String& payload)
{
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
}

View File

@ -7,7 +7,7 @@
#include "AsyncJson.h"
#include "Battery.h"
#include "Configuration.h"
#include "PylontechCanReceiver.h"
#include "MqttHandleBatteryHass.h"
#include "WebApi.h"
#include "WebApi_battery.h"
#include "WebApi_errors.h"
@ -111,4 +111,5 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
request->send(response);
Battery.updateSettings();
MqttHandleBatteryHass.forceUpdate();
}

View File

@ -14,7 +14,7 @@
#include "MqttHandleDtu.h"
#include "MqttHandleHass.h"
#include "MqttHandleVedirectHass.h"
#include "MqttHandlePylontechHass.h"
#include "MqttHandleBatteryHass.h"
#include "MqttHandleInverter.h"
#include "MqttHandleInverterTotal.h"
#include "MqttHandleVedirect.h"
@ -116,6 +116,7 @@ void setup()
MqttHandleVedirect.init(scheduler);
MqttHandleHass.init(scheduler);
MqttHandleVedirectHass.init(scheduler);
MqttHandleBatteryHass.init(scheduler);
MqttHandleHuawei.init(scheduler);
MqttHandlePowerLimiter.init(scheduler);
MessageOutput.println("done");

View File

@ -14,7 +14,7 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"bootstrap-icons-vue": "^1.11.1",
"bootstrap-icons-vue": "^1.11.3",
"mitt": "^3.0.1",
"sortablejs": "^1.15.2",
"spark-md5": "^3.0.2",
@ -24,10 +24,10 @@
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@rushstack/eslint-patch": "^1.7.0",
"@rushstack/eslint-patch": "^1.7.2",
"@tsconfig/node18": "^18.2.2",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.11.5",
"@types/node": "^20.11.7",
"@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.7",
"@types/spark-md5": "^3.0.4",

View File

@ -408,10 +408,10 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.1.tgz#8311b77e6cce322865ba12ada8c3779369610d18"
integrity sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w==
"@rushstack/eslint-patch@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz#b5bc1e081428794f6a4d239707b359404be35ce2"
integrity sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg==
"@rushstack/eslint-patch@^1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9"
integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==
"@tsconfig/node18@^18.2.2":
version "18.2.2"
@ -435,10 +435,10 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/node@^20.11.5":
version "20.11.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e"
integrity sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==
"@types/node@^20.11.7":
version "20.11.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.7.tgz#cb49aedd758c978c30806d0c38b520ed2a3df6e0"
integrity sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==
dependencies:
undici-types "~5.26.4"
@ -855,10 +855,10 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
bootstrap-icons-vue@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.1.tgz#6b0359e8f1a538b2a14e1ced5ba927391f7e3abe"
integrity sha512-vQELXxclY2LauUJ7OMXScv1j+mJS2K2h7NTgW7yDG954dI+SC2QHLKg+Mc7qmCDZ+3oMayeLqmFTDX0GBmdobA==
bootstrap-icons-vue@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.3.tgz#717745c433b2043d6d1ec24260b9bbc9eea16c66"
integrity sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw==
bootstrap@^5.3.2:
version "5.3.2"

Binary file not shown.

Binary file not shown.

Binary file not shown.