diff --git a/README.md b/README.md
index ffa329d4..c0432b66 100644
--- a/README.md
+++ b/README.md
@@ -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!
diff --git a/docs/Display.md b/docs/Display.md
index 154aa05a..c914e4a8 100644
--- a/docs/Display.md
+++ b/docs/Display.md
@@ -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.
-
-
-## 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.
-
-
-## Display Settings
-
-Display settings can also be found in the "Device Manager".
-
\ No newline at end of file
+This documentation will has been moved and can be found here:
diff --git a/docs/UpgradePartition.md b/docs/UpgradePartition.md
index f2cd8bf9..782463f5 100644
--- a/docs/UpgradePartition.md
+++ b/docs/UpgradePartition.md
@@ -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 and enter the "Configuration Management". Recover the previously backuped config files.
+This documentation will has been moved and can be found here:
diff --git a/include/Battery.h b/include/Battery.h
index ffb6f47d..b5f5ace6 100644
--- a/include/Battery.h
+++ b/include/Battery.h
@@ -29,7 +29,6 @@ class BatteryClass {
Task _loopTask;
- uint32_t _lastMqttPublish = 0;
mutable std::mutex _mutex;
std::unique_ptr _upProvider = nullptr;
};
diff --git a/include/BatteryStats.h b/include/BatteryStats.h
index 8ff129f4..36eed06a 100644
--- a/include/BatteryStats.h
+++ b/include/BatteryStats.h
@@ -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:
diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h
index 270ee81d..e49bf9f6 100644
--- a/include/Display_Graphic.h
+++ b/include/Display_Graphic.h
@@ -20,6 +20,7 @@ enum DisplayType_t {
SSD1306,
SH1106,
SSD1309,
+ ST7567_GM12864I_59N,
DisplayType_Max,
};
diff --git a/include/MqttHandlePylontechHass.h b/include/MqttHandleBatteryHass.h
similarity index 78%
rename from include/MqttHandlePylontechHass.h
rename to include/MqttHandleBatteryHass.h
index 64f5a841..f328a6f8 100644
--- a/include/MqttHandlePylontechHass.h
+++ b/include/MqttHandleBatteryHass.h
@@ -4,11 +4,10 @@
#include
#include
-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;
\ No newline at end of file
+extern MqttHandleBatteryHassClass MqttHandleBatteryHass;
diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
index 2978efa7..8047fd12 100644
--- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
+++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
@@ -301,7 +301,20 @@ uint32_t VeDirectFrameHandler::getLastUpdate() const
*/
frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{
- static constexpr frozen::map 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 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);
diff --git a/src/Battery.cpp b/src/Battery.cpp
index 9fdc6e27..381fdc95 100644
--- a/src/Battery.cpp
+++ b/src/Battery.cpp
@@ -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();
}
diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp
index c2c9d9d0..606a372f 100644
--- a/src/BatteryStats.cpp
+++ b/src/BatteryStats.cpp
@@ -5,6 +5,7 @@
#include "Configuration.h"
#include "MqttSettings.h"
#include "JkBmsDataPoints.h"
+#include "MqttSettings.h"
template
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
diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp
index 7f5d94c3..12b2aa56 100644
--- a/src/Display_Graphic.cpp
+++ b/src/Display_Graphic.cpp
@@ -13,6 +13,7 @@ std::map
{ 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);
diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp
index cfc990cd..04ec1e00 100644
--- a/src/InverterSettings.cpp
+++ b/src/InverterSettings.cpp
@@ -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));
}
}
diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp
new file mode 100644
index 00000000..7cad0922
--- /dev/null
+++ b/src/MqttHandleBatteryHass.cpp
@@ -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);
+}
diff --git a/src/MqttHandlePylontechHass.cpp b/src/MqttHandlePylontechHass.cpp
deleted file mode 100644
index d4afbb2d..00000000
--- a/src/MqttHandlePylontechHass.cpp
+++ /dev/null
@@ -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);
-}
diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp
index eec84d7b..b96eb2cf 100644
--- a/src/WebApi_battery.cpp
+++ b/src/WebApi_battery.cpp
@@ -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();
}
diff --git a/src/main.cpp b/src/main.cpp
index 38fcbc2e..dd080832 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -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");
diff --git a/webapp/package.json b/webapp/package.json
index ab152cd4..ac787a8e 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -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",
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index 14f673bd..f1d1b4b4 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -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"
diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz
index be84fc7d..3f62e3dc 100644
Binary files a/webapp_dist/index.html.gz and b/webapp_dist/index.html.gz differ
diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz
index cdb5bb64..8d177806 100644
Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ
diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz
index 02f82db6..6e5a69ea 100644
Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ