diff --git a/include/BatteryStats.h b/include/BatteryStats.h
index 94da35d78..30edb1a06 100644
--- a/include/BatteryStats.h
+++ b/include/BatteryStats.h
@@ -270,3 +270,152 @@ class MqttBatteryStats : public BatteryStats {
         // voltage (if available) is already displayed at the top.
         void getLiveViewData(JsonVariant& root) const final { }
 };
+
+class ZendureBatteryStats : public BatteryStats {
+    friend class ZendureBattery;
+
+    class ZendurePackStats {
+        friend class ZendureBatteryStats;
+
+        // "power": 355,
+        //       "socLevel": 76,
+        //       "state": 2,
+        //       "maxTemp": 3091,
+        //       "totalVol": 4940,
+        //       "maxVol": 330,
+        //       "minVol": 329,
+        //       "softVersion": 4107,
+        //       "sn": "CO4H MAXM C120 099"
+        public:
+            explicit ZendurePackStats(String serial){ setSerial(serial); }
+            void update(JsonObjectConst packData, unsigned int ms);
+            bool isCharging() const { return  _state == 2; };
+            bool isDischarging() const { return  _state == 1; };
+            uint16_t getCapacity() const { return 1920; }
+            std::string getStateString() const { return ZendureBatteryStats::getStateString(_state); };
+
+        protected:
+            bool hasAlarmMaxTemp() const { return _cell_temperature_max >= 45; };
+            bool hasAlarmMinTemp() const { return _cell_temperature_max <= (isCharging() ? 0 : -20); };
+            bool hasAlarmLowSoC() const { return _soc_level < 5; }
+            bool hasAlarmLowVoltage() const { return _voltage_total <= 40.0; }
+            bool hasAlarmHighVoltage() const { return _voltage_total >= 58.4; }
+            void setSerial(String serial);
+
+            String _serial;
+            String _name = "unknown";
+            uint16_t _capacity = 0;
+            uint32_t _version;
+            uint16_t _cell_voltage_min;
+            uint16_t _cell_voltage_max;
+            uint16_t _cell_voltage_spread;
+            float _cell_temperature_max;
+            float _voltage_total;
+            float _current;
+            int16_t _power;
+            uint8_t _soc_level;
+            uint8_t _state;
+
+        private:
+            uint32_t _lastUpdateTimestamp = 0;
+            uint32_t _totalVoltageTimestamp = 0;
+            uint32_t _totalCurrentTimestamp = 0;
+
+    };
+
+    public:
+        virtual ~ZendureBatteryStats(){
+            for (const auto& [key, item] : _packData){
+                delete item;
+            }
+            _packData.clear();
+        }
+        void mqttPublish() const;
+        void getLiveViewData(JsonVariant& root) const;
+
+        bool isCharging() const { return  _state == 1; };
+        bool isDischarging() const { return  _state == 2; };
+
+        static std::string getStateString(uint8_t state);
+
+    protected:
+        std::optional<ZendureBatteryStats::ZendurePackStats*> getPackData(String serial) const;
+        void updatePackData(String serial, JsonObjectConst packData, unsigned int ms);
+        void update(JsonObjectConst props, unsigned int ms);
+        uint16_t getCapacity() const { return _capacity; };
+        uint16_t getAvailableCapacity() const { return getCapacity() * (static_cast<float>(_soc_max - _soc_min) / 100.0); };
+
+    private:
+        std::string getBypassModeString() const;
+        std::string getStateString() const { return ZendureBatteryStats::getStateString(_state); };
+        void calculateEfficiency();
+        void calculateAggregatedPackData();
+
+        void setManufacture(const char* manufacture) {
+            _manufacturer = String(manufacture);
+            if (_device){
+                _manufacturer += " " + _device;
+            }
+        }
+
+        void setSerial(const char* serial) {
+            _serial = String(serial);
+        }
+        void setSerial(String serial) {
+            _serial = serial;
+        }
+
+        void setDevice(const char* device) {
+            _device = String(device);
+        }
+        void setDevice(String device) {
+            _device = device;
+        }
+
+        String _device;
+
+        std::map<String, ZendurePackStats*> _packData = std::map<String, ZendurePackStats*>();
+
+        float _cellTemperature;
+        uint16_t _cellMinMilliVolt;
+        uint16_t _cellMaxMilliVolt;
+        uint16_t _cellDeltaMilliVolt;
+
+        float _soc_max;
+        float _soc_min;
+
+        uint16_t _inverse_max;
+        uint16_t _input_limit;
+        uint16_t _output_limit;
+
+        float _efficiency = 0.0;
+        uint16_t _capacity;
+        uint16_t _charge_power;
+        uint16_t _discharge_power;
+        uint16_t _output_power;
+        uint16_t _input_power;
+        uint16_t _solar_power_1;
+        uint16_t _solar_power_2;
+
+        uint16_t _remain_out_time;
+        uint16_t _remain_in_time;
+
+        uint32_t _swVersion;
+        uint32_t _hwVersion;
+
+        uint8_t _state;
+        uint8_t _num_batteries;
+        uint8_t _bypass_mode;
+        bool _bypass_state;
+        bool _auto_recover;
+        bool _heat_state;
+        bool _auto_shutdown;
+        bool _buzzer;
+
+        bool _alarmLowSoC = false;
+        bool _alarmLowVoltage = false;
+        bool _alarmHightVoltage = false;
+        bool _alarmLowTemperature = false;
+        bool _alarmHighTemperature = false;
+
+};
diff --git a/include/Configuration.h b/include/Configuration.h
index 9f433faf3..b16c2426a 100644
--- a/include/Configuration.h
+++ b/include/Configuration.h
@@ -43,6 +43,8 @@
 #define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256
 #define BATTERY_JSON_MAX_PATH_STRLEN 128
 
+#define ZENDURE_MAX_SERIAL_STRLEN 8
+
 struct CHANNEL_CONFIG_T {
     uint16_t MaxChannelPower;
     char Name[CHAN_MAX_NAME_STRLEN];
@@ -288,6 +290,12 @@ struct CONFIG_T {
         char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
         char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
         BatteryVoltageUnit MqttVoltageUnit;
+        uint8_t ZendureDeviceType;
+        char ZendureDeviceSerial[ZENDURE_MAX_SERIAL_STRLEN + 1];
+        uint8_t ZendureMinSoC;
+        uint8_t ZendureMaxSoC;
+        uint8_t ZendureBypassMode;
+        uint16_t ZendureMaxOutput;
     } Battery;
 
     struct {
diff --git a/include/Utils.h b/include/Utils.h
index a6bc3b15e..fa09874ef 100644
--- a/include/Utils.h
+++ b/include/Utils.h
@@ -21,4 +21,7 @@ class Utils {
     template <typename T>
     static std::optional<T> getNumericValueFromMqttPayload(char const* client,
             std::string const& src, char const* topic, char const* jsonPath);
+
+    template<typename T>
+    static std::optional<T> getJsonElement(JsonObjectConst root, char const* key, size_t nesting = 0);
 };
diff --git a/include/ZendureBattery.h b/include/ZendureBattery.h
new file mode 100644
index 000000000..1edbf0fdf
--- /dev/null
+++ b/include/ZendureBattery.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <optional>
+#include "Battery.h"
+#include <espMqttClient.h>
+
+
+#define ZENDURE_HUB1200     "73bkTV"
+#define ZENDURE_HUB2000     "A8yh63"
+#define ZENDURE_AIO2400     "yWF7hV)"
+#define ZENDURE_ACE1500     "8bM93H"
+#define ZENDURE_HYPER2000   "ja72U0ha)"
+
+
+class ZendureBattery : public BatteryProvider {
+public:
+    ZendureBattery() = default;
+
+    bool init(bool verboseLogging) final;
+    void deinit() final;
+    void loop() final;
+    std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
+
+    uint16_t updateOutputLimit(uint16_t limit);
+
+protected:
+    void timesync();
+
+private:
+    bool _verboseLogging = false;
+
+    uint32_t _updateRateMs;
+    uint64_t _nextUpdate;
+
+    uint32_t _timesyncRateMs;
+    uint64_t _nextTimesync;
+
+    String _deviceId;
+
+    String _baseTopic;
+    String _reportTopic;
+    String _readTopic;
+    String _writeTopic;
+    String _timesyncTopic;
+    String _settingsPayload;
+    std::shared_ptr<ZendureBatteryStats> _stats = std::make_shared<ZendureBatteryStats>();
+
+    void onMqttMessageReport(espMqttClientTypes::MessageProperties const& properties,
+            char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
+};
diff --git a/include/defaults.h b/include/defaults.h
index e348ae63f..1753ad873 100644
--- a/include/defaults.h
+++ b/include/defaults.h
@@ -151,6 +151,11 @@
 #define BATTERY_PROVIDER 0 // Pylontech CAN receiver
 #define BATTERY_JKBMS_INTERFACE 0
 #define BATTERY_JKBMS_POLLING_INTERVAL 5
+#define BATTERY_ZENDURE_DEVICE 0
+#define BATTERY_ZENDURE_MIN_SOC 0
+#define BATTERY_ZENDURE_MAX_SOC 100
+#define BATTERY_ZENDURE_BYPASS_MODE 0
+#define BATTERY_ZENDURE_MAX_OUTPUT 800
 
 #define HUAWEI_ENABLED false
 #define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
diff --git a/src/Battery.cpp b/src/Battery.cpp
index 79ba70020..a65b46b0f 100644
--- a/src/Battery.cpp
+++ b/src/Battery.cpp
@@ -6,6 +6,7 @@
 #include "VictronSmartShunt.h"
 #include "MqttBattery.h"
 #include "PytesCanReceiver.h"
+#include "ZendureBattery.h"
 
 BatteryClass Battery;
 
@@ -61,6 +62,9 @@ void BatteryClass::updateSettings()
         case 4:
             _upProvider = std::make_unique<PytesCanReceiver>();
             break;
+        case 5:
+            _upProvider = std::make_unique<ZendureBattery>();
+            break;
         default:
             MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
             return;
diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp
index 9f32931b7..7c93c3026 100644
--- a/src/BatteryStats.cpp
+++ b/src/BatteryStats.cpp
@@ -6,6 +6,7 @@
 #include "MqttSettings.h"
 #include "JkBmsDataPoints.h"
 #include "MqttSettings.h"
+#include "Utils.h"
 
 template<typename T>
 static void addLiveViewInSection(JsonVariant& root,
@@ -611,3 +612,465 @@ void VictronSmartShuntStats::mqttPublish() const {
     MqttSettings.publish("battery/midpointVoltage", String(_midpointVoltage));
     MqttSettings.publish("battery/midpointDeviation", String(_midpointDeviation));
 }
+
+void ZendureBatteryStats::update(JsonObjectConst props, unsigned int ms){
+    auto soc = Utils::getJsonElement<float>(props, "electricLevel");
+    if (soc.has_value() && (*soc >= 0 && *soc <= 100)){
+        setSoC(*soc, 0/*precision*/, ms);
+    }
+
+    auto soc_max = Utils::getJsonElement<float>(props, "socSet");
+    if (soc_max.has_value()){
+        *soc_max /= 10;
+        if (*soc_max >= 40 && *soc_max <= 100){
+            _soc_max = *soc_max;
+        }
+    }
+
+    auto soc_min = Utils::getJsonElement<float>(props, "minSoc");
+    if (soc_min.has_value()){
+        *soc_min /= 10;
+        if (*soc_min >= 0 && *soc_min <= 60){
+            _soc_min = *soc_min;
+        }
+    }
+
+    auto input_limit = Utils::getJsonElement<uint16_t>(props, "inputLimit");
+    if (input_limit.has_value()){
+        _input_limit = *input_limit;
+    }
+
+    auto output_limit = Utils::getJsonElement<uint16_t>(props, "outputLimit");
+    if (output_limit.has_value()){
+        _output_limit = *output_limit;
+    }
+
+    auto inverse_max = Utils::getJsonElement<uint16_t>(props, "inverseMaxPower");
+    if (inverse_max.has_value()){
+        _inverse_max = *inverse_max;
+    }
+
+    auto pass_mode = Utils::getJsonElement<uint8_t>(props, "passMode");
+    if (pass_mode.has_value()){
+        _bypass_mode = *pass_mode;
+    }
+
+    auto pass_state = Utils::getJsonElement<uint8_t>(props, "pass");
+    if (pass_state.has_value()){
+        _bypass_state = static_cast<bool>(*pass_state);
+    }
+
+    auto batteries = Utils::getJsonElement<uint8_t>(props, "packNum");
+    if (batteries.has_value() && batteries <= 4){
+        _num_batteries = *batteries;
+    }
+
+    auto state = Utils::getJsonElement<uint8_t>(props, "packState");
+    if (state.has_value()){
+        _state = *state;
+    }
+
+    auto heat_state = Utils::getJsonElement<uint8_t>(props, "heatState");
+    if (heat_state.has_value()){
+        _heat_state = *heat_state;
+    }
+
+    auto auto_shutdown = Utils::getJsonElement<uint8_t>(props, "hubState");
+    if (auto_shutdown.has_value()){
+        _auto_shutdown = *auto_shutdown;
+    }
+
+    auto buzzer = Utils::getJsonElement<uint8_t>(props, "buzzerSwitch");
+    if (buzzer.has_value()){
+        _buzzer = *buzzer;
+    }
+
+    auto auto_recover = Utils::getJsonElement<uint8_t>(props, "autoRecover");
+    if (auto_recover.has_value()){
+        _auto_recover = static_cast<bool>(*auto_recover);
+    }
+
+    auto charge_power = Utils::getJsonElement<uint16_t>(props, "outputPackPower");
+    if (charge_power.has_value()){
+        _charge_power = *charge_power;
+    }
+
+    auto discharge_power = Utils::getJsonElement<uint16_t>(props, "packInputPower");
+    if (discharge_power.has_value()){
+        _discharge_power = *discharge_power;
+    }
+
+    auto output_power = Utils::getJsonElement<uint16_t>(props, "outputHomePower");
+    if (output_power.has_value()){
+        _output_power = *output_power;
+    }
+
+    auto input_power = Utils::getJsonElement<uint16_t>(props, "solarInputPower");
+    if (input_power.has_value()){
+        _input_power = *input_power;
+    }
+
+    auto solar_power_1 = Utils::getJsonElement<uint16_t>(props, "solarPower1");
+    if (solar_power_1.has_value()){
+        _solar_power_1 = *solar_power_1;
+    }
+
+    auto solar_power_2 = Utils::getJsonElement<uint16_t>(props, "solarPower2");
+    if (solar_power_2.has_value()){
+        _solar_power_2 = *solar_power_2;
+    }
+
+    float voltage = getVoltage();
+    if (charge_power.has_value() && discharge_power.has_value() && voltage){
+        setCurrent((*charge_power - *discharge_power) / voltage, 2, ms);
+    }
+
+    auto sw_version = Utils::getJsonElement<uint32_t>(props, "masterSoftVersion");
+    if (sw_version.has_value()){
+        _fwversion = String(*sw_version);
+    }
+
+    auto hw_version = Utils::getJsonElement<uint32_t>(props, "masterhaerVersion");
+    if (hw_version.has_value()){
+        _hwversion = String(*hw_version);
+    } else {
+        hw_version = Utils::getJsonElement<uint32_t>(props, "masterHaerVersion");
+        if (hw_version.has_value()){
+            _hwversion = String(*hw_version);
+        }
+    }
+
+    auto outtime = Utils::getJsonElement<uint16_t>(props, "remainOutTime");
+    if (outtime.has_value()){
+        _remain_out_time = *outtime;
+    }
+
+    auto intime = Utils::getJsonElement<uint16_t>(props, "remainInputTime");
+    if (intime.has_value()){
+        _remain_in_time = *intime;
+    }
+
+    calculateEfficiency();
+}
+
+std::optional<ZendureBatteryStats::ZendurePackStats*> ZendureBatteryStats::getPackData(String serial) const {
+    try
+    {
+        return _packData.at(serial);
+    }
+    catch(const std::out_of_range& ex)
+    {
+        return std::nullopt;
+    }
+}
+
+void ZendureBatteryStats::updatePackData(String serial, JsonObjectConst packData, unsigned int ms){
+    try
+    {
+        _packData.at(serial);
+    }
+    catch(const std::out_of_range& ex)
+    {
+        _packData[serial] = new ZendurePackStats(serial);
+    }
+
+    _packData[serial]->update(packData, ms);
+
+
+    calculateAggregatedPackData();
+}
+
+void ZendureBatteryStats::ZendurePackStats::update(JsonObjectConst packData, unsigned int ms){
+    _lastUpdateTimestamp = ms;
+
+    auto power = Utils::getJsonElement<int16_t>(packData, "power");
+    if (power.has_value()){
+        _power = *power;
+    }
+
+    auto soc_level = Utils::getJsonElement<uint8_t>(packData, "socLevel");
+    if (power.has_value()){
+        _soc_level = *soc_level;
+    }
+
+    auto state = Utils::getJsonElement<uint8_t>(packData, "state");
+    if (state.has_value()){
+        _state = *state;
+    }
+
+    auto cell_temp_max = Utils::getJsonElement<float>(packData, "maxTemp");
+    if (state.has_value()){
+        *cell_temp_max -= 2731;
+        *cell_temp_max /= 10;
+
+        if (*cell_temp_max > -100 && *cell_temp_max < 100) {
+            _cell_temperature_max = *cell_temp_max;
+        }
+    }
+
+    auto total_voltage = Utils::getJsonElement<float>(packData, "totalVol");
+    if (total_voltage.has_value()){
+        *total_voltage /= 100;
+        if (*total_voltage > 0 && *total_voltage < 65){
+            _voltage_total = *total_voltage;
+            _totalVoltageTimestamp = _lastUpdateTimestamp;
+        }
+    }
+
+    auto cell_voltage_max = Utils::getJsonElement<uint16_t>(packData, "maxVol");
+    if (cell_voltage_max.has_value()){
+        *cell_voltage_max *= 10;
+        if (*cell_voltage_max > 2000 && *cell_voltage_max < 4000){
+            _cell_voltage_max = *cell_voltage_max;
+        }
+    }
+
+    auto cell_voltage_min = Utils::getJsonElement<uint16_t>(packData, "minVol");
+    if (cell_voltage_min.has_value()){
+        *cell_voltage_min *= 10;
+        if (*cell_voltage_min > 2000 && *cell_voltage_min < 4000){
+            _cell_voltage_min = *cell_voltage_min;
+        }
+    }
+
+    auto version = Utils::getJsonElement<uint32_t>(packData, "softVersion");
+    if (version.has_value()){
+        _version = *version;
+    }
+
+    _cell_voltage_spread = _cell_voltage_max - _cell_voltage_min;
+
+    if (_voltage_total){
+        _current = static_cast<float>(_power) / _voltage_total;
+    }
+}
+
+void ZendureBatteryStats::getLiveViewData(JsonVariant& root) const {
+    BatteryStats::getLiveViewData(root);
+
+
+    auto bool2str = [](bool value) -> std::string {
+        return value ? "enabled" : "disabled";
+    };
+
+    auto addRemainingTime = [](auto root, auto section, const char* name, uint16_t value) {
+        if (value >= 59940){
+            addLiveViewTextInSection(root, section, name, "unavail");
+        }else{
+            addLiveViewInSection(root, section, name, value, "min", 0);
+        }
+    };
+
+    // values go into the "Status" card of the web application
+    std::string section("status");
+    addLiveViewInSection(root, section, "totalInputPower", _input_power, "W", 0);
+    addLiveViewInSection(root, section, "chargePower", _charge_power, "W", 0);
+    addLiveViewInSection(root, section, "dischargePower", _discharge_power, "W", 0);
+    addLiveViewInSection(root, section, "totalOutputPower", _output_power, "W", 0);
+    addLiveViewInSection(root, section, "efficiency", _efficiency, "%", 3);
+    addLiveViewInSection(root, section, "capacity", _capacity, "Wh", 0);
+    addLiveViewInSection(root, section, "availableCapacity", getAvailableCapacity(), "Wh", 0);
+    addLiveViewTextInSection(root, section, "state", getStateString());
+    addLiveViewTextInSection(root, section, "heatState", bool2str(_heat_state));
+    addLiveViewTextInSection(root, section, "bypassState", bool2str(_bypass_state));
+    addLiveViewInSection(root, section, "batteries", _num_batteries, "", 0);
+    addRemainingTime(root, section, "remainOutTime", _remain_out_time);
+    addRemainingTime(root, section, "remainInTime", _remain_in_time);
+
+    // values go into the "Settings" card of the web application
+    section = "settings";
+    addLiveViewInSection(root, section, "maxInversePower", _inverse_max, "W", 0);
+    addLiveViewInSection(root, section, "outputLimit", _output_limit, "W", 0);
+    addLiveViewInSection(root, section, "inputLimit", _output_limit, "W", 0);
+    addLiveViewInSection(root, section, "minSoC", _soc_min, "%", 1);
+    addLiveViewInSection(root, section, "maxSoC", _soc_max, "%", 1);
+    addLiveViewTextInSection(root, section, "autoRecover", bool2str(_auto_recover));
+    addLiveViewTextInSection(root, section, "autoShutdown", bool2str(_auto_shutdown));
+    addLiveViewTextInSection(root, section, "bypassMode", getBypassModeString());
+    addLiveViewTextInSection(root, section, "buzzer", bool2str(_buzzer));
+
+    // values go into the "Solar Panels" card of the web application
+    section = "panels";
+    addLiveViewInSection(root, section, "solarInputPower1", _solar_power_1, "W", 0);
+    addLiveViewInSection(root, section, "solarInputPower2", _solar_power_2, "W", 0);
+
+    // pack data goes to dedicated cards of the web application
+    char buff[30];
+    for (const auto& [sn, value] : _packData){
+        snprintf(buff, sizeof(buff), "_%s [%s]", value->_name.c_str(), sn.c_str());
+        section = std::string(buff);
+        addLiveViewTextInSection(root, section, "state", value->getStateString());
+        addLiveViewInSection(root, section, "cellMaxTemperature", value->_cell_temperature_max, "°C", 1);
+        addLiveViewInSection(root, section, "cellMinVoltage", value->_cell_voltage_min, "mV", 0);
+        addLiveViewInSection(root, section, "cellMaxVoltage", value->_cell_voltage_max, "mV", 0);
+        addLiveViewInSection(root, section, "cellDiffVoltage", value->_cell_voltage_spread, "mV", 0);
+        addLiveViewInSection(root, section, "voltage", value->_voltage_total, "V", 2);
+        addLiveViewInSection(root, section, "power", value->_power, "W", 0);
+        addLiveViewInSection(root, section, "current", value->_current, "A", 2);
+        addLiveViewInSection(root, section, "SoC", value->_soc_level, "%", 1);
+        addLiveViewInSection(root, section, "capacity", value->_capacity, "Wh", 0);
+        addLiveViewInSection(root, section, "FwVersion", value->_version, "", 0);
+    }
+
+    // values go into the alarms card of the web application
+    addLiveViewAlarm(root, "lowSOC", _alarmLowSoC);
+    addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage);
+    addLiveViewAlarm(root, "highVoltage", _alarmHightVoltage);
+    addLiveViewAlarm(root, "underTemperatureCharge", _alarmLowTemperature);
+    addLiveViewAlarm(root, "overTemperatureCharge", _alarmHighTemperature);
+}
+
+void ZendureBatteryStats::mqttPublish() const {
+    BatteryStats::mqttPublish();
+
+    MqttSettings.publish("battery/cellMinMilliVolt", String(_cellMinMilliVolt));
+    MqttSettings.publish("battery/cellMaxMilliVolt", String(_cellMaxMilliVolt));
+    MqttSettings.publish("battery/cellDiffMilliVolt", String(_cellDeltaMilliVolt));
+    MqttSettings.publish("battery/cellMaxTemperature", String(_cellTemperature));
+    MqttSettings.publish("battery/chargePower", String(_charge_power));
+    MqttSettings.publish("battery/dischargePower", String(_discharge_power));
+    MqttSettings.publish("battery/heating", String(static_cast<uint8_t>(_heat_state)));
+    MqttSettings.publish("battery/state", String(_state));
+    MqttSettings.publish("battery/numPacks", String(_num_batteries));
+
+    for (const auto& [sn, value] : _packData){
+        MqttSettings.publish("battery/" + sn + "/cellMinMilliVolt", String(value->_cell_voltage_min));
+        MqttSettings.publish("battery/" + sn + "/cellMaxMilliVolt", String(value->_cell_voltage_max));
+        MqttSettings.publish("battery/" + sn + "/cellDiffMilliVolt", String(value->_cell_voltage_spread));
+        MqttSettings.publish("battery/" + sn + "/cellMaxTemperature", String(value->_cell_temperature_max));
+        MqttSettings.publish("battery/" + sn + "/voltage", String(value->_voltage_total));
+        MqttSettings.publish("battery/" + sn + "/power", String(value->_power));
+        MqttSettings.publish("battery/" + sn + "/current", String(value->_current));
+        MqttSettings.publish("battery/" + sn + "/stateOfCharge", String(value->_soc_level));
+        MqttSettings.publish("battery/" + sn + "/state", String(value->_state));
+    }
+
+    MqttSettings.publish("battery/solarPowerMppt1", String(_solar_power_1));
+    MqttSettings.publish("battery/solarPowerMppt2", String(_solar_power_2));
+    MqttSettings.publish("battery/outputPower", String(_output_power));
+    MqttSettings.publish("battery/inputPower", String(_input_power));
+    MqttSettings.publish("battery/outputLimitPower", String(_output_limit));
+   // MqttSettings.publish("battery/inputLimitPower", String(_output_limit));
+    MqttSettings.publish("battery/bypass", String(static_cast<uint8_t>(_bypass_state)));
+}
+
+std::string ZendureBatteryStats::getBypassModeString() const {
+    switch (_bypass_mode) {
+        case 0:
+            return "auto";
+        case 1:
+            return "alwaysoff";
+        case 2:
+            return "alwayson";
+        default:
+            return "invalid";
+    }
+}
+
+std::string ZendureBatteryStats::getStateString(uint8_t state) {
+    switch (state) {
+        case 0:
+            return "idle";
+        case 1:
+            return "charging";
+        case 2:
+            return "discharging";
+        default:
+            return "invalid";
+    }
+}
+
+void ZendureBatteryStats::calculateAggregatedPackData() {
+    // calcualte average voltage over all battery packs
+    float voltage = 0.0;
+    float temp = 0.0;
+    uint32_t cellMin = UINT32_MAX;
+    uint32_t cellMax = 0;
+    uint16_t capacity = 0;
+
+    uint32_t timestampVoltage = 0;
+
+    size_t countVoltage = 0;
+    size_t countValid = 0;
+
+
+    bool alarmLowSoC = false;
+    bool alarmTempLow = false;
+    bool alarmTempHigh = false;
+    bool alarmLowVoltage = false;
+    bool alarmHighVoltage = false;
+
+    for (const auto& [sn, value] : _packData){
+        capacity += value->getCapacity();
+
+        // sum all pack voltages
+        if (value->_totalVoltageTimestamp){
+            voltage += value->_voltage_total;
+            timestampVoltage = max(timestampVoltage, value->_totalVoltageTimestamp);
+
+            alarmLowVoltage |= value->hasAlarmLowVoltage();
+            alarmHighVoltage |= value->hasAlarmHighVoltage();
+
+            countVoltage++;
+        }
+
+        // aggregate remaining values
+        if (value->_lastUpdateTimestamp){
+            temp = max(temp, value->_cell_temperature_max);
+
+            cellMax = max(cellMax, static_cast<uint32_t>(value->_cell_voltage_max));
+            if (value->_cell_voltage_min){
+                cellMin = min(cellMin, static_cast<uint32_t>(value->_cell_voltage_min));
+            }
+
+            alarmLowSoC |= value->hasAlarmLowSoC();
+            alarmTempLow |= value->hasAlarmMinTemp();
+            alarmTempHigh |= value->hasAlarmMaxTemp();
+
+            countValid++;
+        }
+    }
+
+    if (countVoltage){
+        setVoltage(voltage / countVoltage, timestampVoltage);
+
+        _alarmLowVoltage = alarmLowVoltage;
+        _alarmHightVoltage = alarmHighVoltage;
+    }
+
+    if(countValid){
+        _cellMaxMilliVolt = static_cast<uint16_t>(cellMax);
+        _cellMinMilliVolt = static_cast<uint16_t>(cellMin);
+        _cellDeltaMilliVolt = _cellMaxMilliVolt - _cellMinMilliVolt;
+
+        _cellTemperature = temp;
+        _alarmHighTemperature = alarmTempHigh;
+        _alarmLowTemperature = alarmTempLow;
+        _alarmLowSoC = alarmLowSoC;
+    }
+
+    _capacity = capacity;
+}
+
+void ZendureBatteryStats::calculateEfficiency() {
+    float in = _input_power;
+    float out = _output_power;
+    if (isCharging()){
+        out += _charge_power;
+    }
+    if (isDischarging()){
+        in += _discharge_power;
+    }
+
+    _efficiency = in ? out / in : 0.0;
+    _efficiency *= 100;
+}
+
+
+void ZendureBatteryStats::ZendurePackStats::setSerial(String serial){
+    if (serial.startsWith("CO4H")){
+        _capacity = 1920;
+        _name = "AB2000";
+    }
+    _serial = serial;
+}
diff --git a/src/Configuration.cpp b/src/Configuration.cpp
index 9b3c7d4e4..b5d36fb4d 100644
--- a/src/Configuration.cpp
+++ b/src/Configuration.cpp
@@ -261,6 +261,12 @@ bool ConfigurationClass::write()
     battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
     battery["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath;
     battery["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit;
+    battery["zendure_device_type"] = config.Battery.ZendureDeviceType;
+    battery["zendure_device_serial"] = config.Battery.ZendureDeviceSerial;
+    battery["zendure_soc_min"] = config.Battery.ZendureMinSoC;
+    battery["zendure_soc_max"] = config.Battery.ZendureMaxSoC;
+    battery["zendure_bypass_mode"] = config.Battery.ZendureBypassMode;
+    battery["zendure_max_output"] = config.Battery.ZendureMaxOutput;
 
     JsonObject huawei = doc["huawei"].to<JsonObject>();
     huawei["enabled"] = config.Huawei.Enabled;
@@ -611,6 +617,12 @@ bool ConfigurationClass::read()
     strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
     strlcpy(config.Battery.MqttVoltageJsonPath, battery["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath));
     config.Battery.MqttVoltageUnit = battery["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
+    config.Battery.ZendureDeviceType = battery["zendure_device_type"] | BATTERY_ZENDURE_DEVICE;
+    strlcpy(config.Battery.ZendureDeviceSerial, battery["zendure_device_serial"] | "", sizeof(config.Battery.ZendureDeviceSerial));
+    config.Battery.ZendureMinSoC = battery["zendure_soc_min"] | BATTERY_ZENDURE_MIN_SOC;
+    config.Battery.ZendureMaxSoC = battery["zendure_soc_max"] | BATTERY_ZENDURE_MAX_SOC;
+    config.Battery.ZendureBypassMode = battery["zendure_bypass_mode"] | BATTERY_ZENDURE_BYPASS_MODE;
+    config.Battery.ZendureMaxOutput = battery["zendure_max_output"] | BATTERY_ZENDURE_MAX_OUTPUT;
 
     JsonObject huawei = doc["huawei"];
     config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index c2e40885b..e87e3efc1 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -247,3 +247,29 @@ std::optional<T> Utils::getNumericValueFromMqttPayload(char const* client,
 
 template std::optional<float> Utils::getNumericValueFromMqttPayload(char const* client,
         std::string const& src, char const* topic, char const* jsonPath);
+
+template<typename T>
+std::optional<T> Utils::getJsonElement(JsonObjectConst const root, char const* key, size_t nesting /* = 0*/) {
+    if (root.containsKey(key)){
+
+        auto item = root[key].as<JsonVariantConst>();
+
+        if (item.is<T>() && item.nesting() == nesting){
+            return item.as<T>();
+        }
+    }
+    return std::nullopt;
+}
+
+template std::optional<const char*> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<String> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<std::string> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<uint8_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<uint16_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<uint32_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<int8_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<int16_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<int32_t> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<float> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<JsonObjectConst> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
+template std::optional<JsonArrayConst> Utils::getJsonElement(JsonObjectConst root, char const* key, size_t nesting /* = 0*/);
diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp
index aa8040d73..02bb0e4bd 100644
--- a/src/WebApi_battery.cpp
+++ b/src/WebApi_battery.cpp
@@ -45,6 +45,12 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
     root["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
     root["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath;
     root["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit;
+    root["zendure_device_type"] = config.Battery.ZendureDeviceType;
+    root["zendure_device_serial"] = config.Battery.ZendureDeviceSerial;
+    root["zendure_soc_min"] = config.Battery.ZendureMinSoC;
+    root["zendure_soc_max"] = config.Battery.ZendureMaxSoC;
+    root["zendure_bypass_mode"] = config.Battery.ZendureBypassMode;
+    root["zendure_max_output"] = config.Battery.ZendureMaxOutput;
 
     response->setLength();
     request->send(response);
@@ -91,6 +97,12 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
     strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as<String>().c_str(), sizeof(config.Battery.MqttVoltageTopic));
     strlcpy(config.Battery.MqttVoltageJsonPath, root["mqtt_voltage_json_path"].as<String>().c_str(), sizeof(config.Battery.MqttVoltageJsonPath));
     config.Battery.MqttVoltageUnit = static_cast<BatteryVoltageUnit>(root["mqtt_voltage_unit"].as<uint8_t>());
+    config.Battery.ZendureDeviceType = root["zendure_device_type"].as<uint8_t>();
+    strlcpy(config.Battery.ZendureDeviceSerial, root["zendure_device_serial"].as<String>().c_str(), sizeof(config.Battery.ZendureDeviceSerial));
+    config.Battery.ZendureMinSoC = root["zendure_soc_min"].as<uint8_t>();
+    config.Battery.ZendureMaxSoC = root["zendure_soc_max"].as<uint8_t>();
+    config.Battery.ZendureBypassMode = root["zendure_bypass_mode"].as<uint8_t>();
+    config.Battery.ZendureMaxOutput = root["zendure_max_output"].as<uint16_t>();
 
     WebApi.writeConfig(retMsg);
 
diff --git a/src/ZendureBattery.cpp b/src/ZendureBattery.cpp
new file mode 100644
index 000000000..a277cb420
--- /dev/null
+++ b/src/ZendureBattery.cpp
@@ -0,0 +1,218 @@
+#include <functional>
+
+#include "Configuration.h"
+#include "ZendureBattery.h"
+#include "MqttSettings.h"
+#include "MessageOutput.h"
+#include "Utils.h"
+
+bool ZendureBattery::init(bool verboseLogging)
+{
+    _verboseLogging = verboseLogging;
+
+    auto const& config = Configuration.get();
+    String deviceType;
+    String deviceName;
+
+    switch (config.Battery.ZendureDeviceType){
+        case 0:
+            deviceType = ZENDURE_HUB1200;
+            deviceName = String("HUB 1200");
+            break;
+        case 1:
+            deviceType = ZENDURE_HUB2000;
+            deviceName = String("HUB 2000");
+            break;
+        case 2:
+            deviceType = ZENDURE_AIO2400;
+            deviceName = String("AIO 2400");
+            break;
+        case 3:
+            deviceType = ZENDURE_ACE1500;
+            deviceName = String("Ace 1500");
+            break;
+        case 4:
+            deviceType = ZENDURE_HYPER2000;
+            deviceName = String("Hyper 2000");
+            break;
+        default:
+            MessageOutput.printf("ZendureBattery: Invalid device type!");
+            return false;
+    }
+
+    //_baseTopic = "/73bkTV/sU59jtkw/";
+    if (strlen(config.Battery.ZendureDeviceSerial) != 8) {
+        MessageOutput.printf("ZendureBattery: Invalid serial number!");
+        return false;
+    }
+
+    _deviceId = config.Battery.ZendureDeviceSerial;
+
+    _baseTopic = "/" + deviceType + "/" + _deviceId + "/";
+    _reportTopic = _baseTopic + "properties/report";
+    _readTopic = "iot" + _baseTopic + "properties/read";
+    _writeTopic = "iot" + _baseTopic + "properties/write";
+    _timesyncTopic = "iot" + _baseTopic + "time-sync/reply";
+
+    MqttSettings.subscribe(_reportTopic, 0/*QoS*/,
+            std::bind(&ZendureBattery::onMqttMessageReport,
+                this, std::placeholders::_1, std::placeholders::_2,
+                std::placeholders::_3, std::placeholders::_4,
+                std::placeholders::_5, std::placeholders::_6)
+            );
+
+    _updateRateMs = 15 * 1000;
+    _timesyncRateMs = 60 * 60 * 1000;
+    _nextUpdate = 0;
+    _nextTimesync = 0;
+
+    _stats->setSerial(_deviceId);
+    _stats->setDevice(deviceName);
+    _stats->setManufacture("Zendure");
+
+    JsonDocument root;
+    JsonVariant prop = root["properties"].to<JsonObject>();
+
+    prop["pvBrand"] = 1;
+    prop["autoRecover"] = 1;
+    prop["buzzerSwitch"] = 0;
+    prop["passMode"] = config.Battery.ZendureBypassMode;
+    prop["socSet"] = config.Battery.ZendureMaxSoC * 10;
+    prop["minSoc"] = config.Battery.ZendureMinSoC * 10;
+    //prop["outputLimit"] = config.Battery.ZendureMaxOutput;
+    prop["inverseMaxPower"] = config.Battery.ZendureMaxOutput;
+    prop["smartMode"] = 0;
+    prop["autoModel"] = 0;
+
+    serializeJson(root, _settingsPayload);
+
+    if (_verboseLogging) {
+        MessageOutput.printf("ZendureBattery: Subscribed to '%s' for status readings\r\n", _reportTopic.c_str());
+    }
+
+    return true;
+}
+
+void ZendureBattery::deinit()
+{
+    if (!_reportTopic.isEmpty()) {
+        MqttSettings.unsubscribe(_reportTopic);
+    }
+}
+
+void ZendureBattery::loop()
+{
+    auto ms = millis();
+
+    if (ms >= _nextUpdate) {
+        _nextUpdate = ms + _updateRateMs;
+        if (!_readTopic.isEmpty()) {
+            MqttSettings.publishGeneric(_readTopic, "{\"properties\": [\"getAll\"] }", false, 0);
+        }
+    }
+
+    if (ms >= _nextTimesync) {
+        _nextTimesync = ms + _timesyncRateMs;
+        timesync();
+
+        // republish settings - just to be sure
+        if (!_writeTopic.isEmpty()) {
+            if (!_settingsPayload.isEmpty()){
+                MqttSettings.publishGeneric(_writeTopic, _settingsPayload, false, 0);
+            }
+        }
+    }
+}
+uint16_t ZendureBattery::updateOutputLimit(uint16_t limit)
+{
+    if (_writeTopic.isEmpty()) {
+        return _stats->_output_limit;
+    }
+
+    if (_stats->_output_limit != limit){
+        if (limit < 100 && limit != 0){
+            uint16_t base = limit / 30U;
+            uint16_t remain = (limit % 30U) / 15U;
+            limit = 30 * base + 30 * remain;
+        }
+        MqttSettings.publishGeneric(_writeTopic, "{\"properties\": {\"outputLimit\": " + String(limit) + "} }", false, 0);
+    }
+
+    return limit;
+}
+
+void ZendureBattery::timesync()
+{
+    if (!_timesyncTopic.isEmpty()) {
+        struct tm timeinfo;
+        if (getLocalTime(&timeinfo, 5)) {
+            char timeStringBuff[50];
+            strftime(timeStringBuff, sizeof(timeStringBuff), "%s", &timeinfo);
+            MqttSettings.publishGeneric(_timesyncTopic,"{\"zoneOffset\": \"+00:00\", \"messageId\": 123, \"timestamp\": " + String(timeStringBuff) + "}", false, 0);
+        }
+    }
+}
+
+void ZendureBattery::onMqttMessageReport(espMqttClientTypes::MessageProperties const& properties,
+        char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
+{
+    auto ms = millis();
+
+    std::string const src = std::string(reinterpret_cast<const char*>(payload), len);
+    std::string logValue = src.substr(0, 64);
+    if (src.length() > logValue.length()) { logValue += "..."; }
+
+    auto log = [_verboseLogging=_verboseLogging](char const* format, auto&&... args) -> void {
+        if (_verboseLogging) {
+            MessageOutput.printf("ZendureBattery: ");
+            MessageOutput.printf(format, args...);
+            MessageOutput.println();
+        }
+        return;
+    };
+
+    JsonDocument json;
+
+    const DeserializationError error = deserializeJson(json, src);
+    if (error) {
+        return log("cannot parse payload '%s' as JSON", logValue.c_str());
+    }
+
+    if (json.overflowed()) {
+        return log("payload too large to process as JSON");
+    }
+
+    auto obj = json.as<JsonObjectConst>();
+
+    // validate input data
+    // messageId has to be set to "123"
+    // deviceId has to be set to the configured deviceId
+    // product has to be set to "solarFlow"
+    if (!json["messageId"].as<String>().equals("123")){
+        return log("Invalid or missing 'messageId' in '%s'", logValue.c_str());
+    }
+    if (!json["deviceId"].as<String>().equals(_deviceId)){
+        return log("Invalid or missing 'deviceId' in '%s'", logValue.c_str());
+    }
+    if (!json["product"].as<String>().equals("solarFlow")){
+        return log("Invalid or missing 'product' in '%s'", logValue.c_str());
+    }
+
+    auto packData = Utils::getJsonElement<JsonArrayConst>(obj, "packData", 2);
+    if (packData.has_value()){
+        for (JsonObjectConst battery : *packData) {
+            auto serial = Utils::getJsonElement<String>(battery, "sn");
+            if (serial.has_value() && (*serial).length() == 15){
+                _stats->updatePackData(*serial, battery, ms);
+            }else{
+                log("Invalid or missing serial of battery pack in '%s'", logValue.c_str());
+            }
+
+        }
+    }
+
+    auto props = Utils::getJsonElement<JsonObjectConst>(obj, "properties", 1);
+    if (props.has_value()){
+        _stats->update(*props, ms);
+    }
+}
diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue
index d5b180fe5..2d184b91d 100644
--- a/webapp/src/components/BatteryView.vue
+++ b/webapp/src/components/BatteryView.vue
@@ -47,7 +47,14 @@
                                 class="col order-0"
                             >
                                 <div class="card" :class="{ 'border-info': true }">
-                                    <div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
+                                    <div class="card-header text-bg-info">
+                                        <template v-if="section.toString().startsWith('_')">
+                                            {{ section.toString().substring(1) }}
+                                        </template>
+                                        <template v-else>
+                                            {{ $t('battery.' + section) }}
+                                        </template>
+                                    </div>
                                     <div class="card-body">
                                         <table class="table table-striped table-hover">
                                             <thead>
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json
index 7771a0c22..03c8d143c 100644
--- a/webapp/src/locales/de.json
+++ b/webapp/src/locales/de.json
@@ -672,6 +672,7 @@
         "ProviderMqtt": "Batteriewerte aus MQTT Broker",
         "ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
         "ProviderPytesCan": "Pytes per CAN-Bus",
+        "ProviderZendureLocalMqtt": "Zendure per lokalem MQTT Broker",
         "MqttSocConfiguration": "Einstellungen SoC",
         "MqttVoltageConfiguration": "Einstellungen Spannung",
         "MqttJsonPath": "Optional: JSON-Pfad",
@@ -683,8 +684,20 @@
         "JkBmsInterface": "Schnittstellentyp",
         "JkBmsInterfaceUart": "TTL-UART an der MCU",
         "JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
+        "ZendureConfiguration": "Einstellungen",
+        "ZendureDeviceType": "Produkt",
+        "ZendureDeviceSerial": "Seriennummer",
+        "ZendureMinSoc": "Minimaler Ladezustand",
+        "ZendureMaxSoc": "Maximaler Ladezustand",
+        "ZendureBypassMode": "Bypass Modus",
+        "ZendureBypassModeAutomatic": "Automatisch",
+        "ZendureBypassModeAlwaysOff": "Dauerhaft Ausgeschaltet",
+        "ZendureBypassModeAlwaysOn": "Dauerhaft Eingeschaltet",
+        "ZendureMaxOutput": "Limitierung Ausgangsleistung",
         "PollingInterval": "Abfrageintervall",
-        "Seconds": "@:base.Seconds"
+        "Seconds": "@:base.Seconds",
+        "Percent": "%",
+        "Watt": "W"
     },
     "inverteradmin": {
         "InverterSettings": "Wechselrichter Einstellungen",
@@ -989,6 +1002,39 @@
         "consumedAmpHours": "Verbrauchte Amperestunden",
         "midpointVoltage": "Mittelpunktspannung",
         "midpointDeviation": "Mittelpunktsabweichung",
-        "lastFullCharge": "Letztes mal Vollgeladen"
+        "lastFullCharge": "Letztes mal Vollgeladen",
+        "solarInputPower1": "Leistung MPPT1",
+        "solarInputPower2": "Leistung MPPT2",
+        "totalInputPower": "Eingangsleistung",
+        "chargePower": "Ladeleistung",
+        "dischargePower": "Entladeleistung",
+        "totalOutputPower": "Ausgangsleistung",
+        "maxInversePower": "Maximale Ausgangsleistung",
+        "outputLimit": "Limit Ausgangsleistung",
+        "inputLimit": "Limit Eingangsleistung",
+        "minSoC": "Minimaler Ladezustand",
+        "maxSoC": "Maximaler Ladezustand",
+        "state": "Aktueller Bertriebsmodus",
+        "bypassMode": "Bypass Modus",
+        "bypassState": "Bypass Status",
+        "heatState": "Batterieheizung",
+        "autoRecover": "Automatischer Wiederanlauf",
+        "autoShutdown": "Automatisches Herunterfahren",
+        "buzzer": "Integrierter Summer",
+        "batteries": "Anzahl installierter Batterien",
+        "charging": "Laden",
+        "discharging": "Entladen",
+        "idle": "Leerlauf",
+        "invalid": "Ungültig",
+        "enabled": "Aktiviert",
+        "disabled": "Deaktiviert",
+        "alwaysoff": "Dauerhaft Ausgeschaltet",
+        "alwayson": "Dauerhaft Eingeschaltet",
+        "efficiency": "Wirkungsgrad",
+        "panels": "Solar Eingänge",
+        "settings": "Einstellungen",
+        "remainOutTime": "Verbleibende Entladezeit",
+        "remainInTime": "Verbleibende Ladezeit",
+        "unavail": "N/A"
     }
 }
diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json
index 0be700f01..ad7823446 100644
--- a/webapp/src/locales/en.json
+++ b/webapp/src/locales/en.json
@@ -674,6 +674,7 @@
         "ProviderMqtt": "Battery data from MQTT broker",
         "ProviderVictron": "Victron SmartShunt using VE.Direct interface",
         "ProviderPytesCan": "Pytes using CAN bus",
+        "ProviderZendureLocalMqtt": "Zendure using local MQTT broker",
         "MqttConfiguration": "MQTT Settings",
         "MqttSocConfiguration": "SoC Settings",
         "MqttVoltageConfiguration": "Voltage Settings",
@@ -686,8 +687,20 @@
         "JkBmsInterface": "Interface Type",
         "JkBmsInterfaceUart": "TTL-UART on MCU",
         "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
+        "ZendureConfiguration": "Configuration",
+        "ZendureDeviceType": "Product type",
+        "ZendureDeviceSerial": "Serialnumber",
+        "ZendureMinSoc": "Minimum SoC",
+        "ZendureMaxSoc": "Maximum SoC",
+        "ZendureBypassMode": "Bypass mode",
+        "ZendureBypassModeAutomatic": "Automatic",
+        "ZendureBypassModeAlwaysOff": "Always Off",
+        "ZendureBypassModeAlwaysOn": "Always On",
+        "ZendureMaxOutput": "Maximum output power",
         "PollingInterval": "Polling Interval",
-        "Seconds": "@:base.Seconds"
+        "Seconds": "@:base.Seconds",
+        "Percent": "%",
+        "Watt": "W"
     },
     "inverteradmin": {
         "InverterSettings": "Inverter Settings",
@@ -993,6 +1006,40 @@
         "consumedAmpHours": "Consumed Amp Hours",
         "midpointVoltage": "Midpoint Voltage",
         "midpointDeviation": "Midpoint Deviation",
-        "lastFullCharge": "Last full Charge"
+        "lastFullCharge": "Last full Charge",
+        "solarInputPower1": "MPPT1 power",
+        "solarInputPower2": "MPPT2 power",
+        "totalInputPower": "Total input power",
+        "chargePower": "Charge power",
+        "dischargePower": "Discharge power",
+        "totalOutputPower": "Total output power",
+        "maxInversePower": "Maximum output power",
+        "outputLimit": "Output power limit",
+        "inputLimit": "Input power limit",
+        "minSoC": "Minimal State of Charge",
+        "maxSoC": "Maximal State of Charge",
+        "state": "Current state of operation",
+        "bypassMode": "Bypass mode",
+        "bypassState": "Bypass switch",
+        "heatState": "Battery heating",
+        "autoRecover": "Automatic recover",
+        "autoShutdown": "Automatic shutdown",
+        "buzzer": "Integrated buzzer",
+        "batteries": "Number of batteries",
+        "charging": "Charging",
+        "discharging": "Discharging",
+        "idle": "Idle",
+        "invalid": "Invalid",
+        "enabled": "Enabled",
+        "disabled": "Disabled",
+        "auto": "Automatic",
+        "alwaysoff": "Always Off",
+        "alwayson": "Always On",
+        "efficiency": "Efficiency",
+        "panels": "Solar input",
+        "settings": "Settings",
+        "remainOutTime": "Remaining discharge time",
+        "remainInTime": "Remaining charge time",
+        "unavail": "N/A"
     }
 }
diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json
index 316d19d69..e1527be51 100644
--- a/webapp/src/locales/fr.json
+++ b/webapp/src/locales/fr.json
@@ -947,6 +947,40 @@
         "consumedAmpHours": "Consumed Amp Hours",
         "midpointVoltage": "Midpoint Voltage",
         "midpointDeviation": "Midpoint Deviation",
-        "lastFullCharge": "Last full Charge"
+        "lastFullCharge": "Last full Charge",
+        "solarInputPower1": "MPPT1 power",
+        "solarInputPower2": "MPPT2 power",
+        "totalInputPower": "Total input power",
+        "chargePower": "Charge power",
+        "dischargePower": "Discharge power",
+        "totalOutputPower": "Total output power",
+        "maxInversePower": "Maximum output power",
+        "outputLimit": "Output power limit",
+        "inputLimit": "Input power limit",
+        "minSoC": "Minimal State of Charge",
+        "maxSoC": "Maximal State of Charge",
+        "state": "Current state of operation",
+        "bypassMode": "Bypass mode",
+        "bypassState": "Bypass switch",
+        "heatState": "Battery heating",
+        "autoRecover": "Automatic recover",
+        "autoShutdown": "Automatic shutdown",
+        "buzzer": "Integrated buzzer",
+        "batteries": "Number of batteries",
+        "charging": "Charging",
+        "discharging": "Discharging",
+        "idle": "Idle",
+        "invalid": "Invalid",
+        "enabled": "Enabled",
+        "disabled": "Disabled",
+        "auto": "Automatic",
+        "alwaysoff": "Always Off",
+        "alwayson": "Always On",
+        "efficiency": "Efficiency",
+        "panels": "Solar input",
+        "settings": "Settings",
+        "remainOutTime": "Remaining discharge time",
+        "remainInTime": "Remaining charge time",
+        "unavail": "N/A"
     }
 }
diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts
index 348ed2a32..7973a77e5 100644
--- a/webapp/src/types/BatteryConfig.ts
+++ b/webapp/src/types/BatteryConfig.ts
@@ -9,4 +9,10 @@ export interface BatteryConfig {
     mqtt_voltage_topic: string;
     mqtt_voltage_json_path: string;
     mqtt_voltage_unit: number;
+    zendure_device_type: number;
+    zendure_device_serial: string;
+    zendure_soc_min: number;
+    zendure_soc_max: number;
+    zendure_bypass_mode: number;
+    zendure_max_output: number;
 }
diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue
index bd54fbc08..972443c5b 100644
--- a/webapp/src/views/BatteryAdminView.vue
+++ b/webapp/src/views/BatteryAdminView.vue
@@ -120,6 +120,82 @@
                 </CardElement>
             </template>
 
+            <template v-if="batteryConfigList.enabled && batteryConfigList.provider == 5">
+                <CardElement :text="$t('batteryadmin.ZendureConfiguration')" textVariant="text-bg-primary" addSpace>
+                    <div class="row mb-3">
+                        <label for="zendure_device_type" class="col-sm-2 col-form-label">
+                            {{ $t('batteryadmin.ZendureDeviceType') }}
+                        </label>
+                        <div class="col-sm-10">
+                            <select
+                                id="zendure_device_type"
+                                class="form-select"
+                                v-model="batteryConfigList.zendure_device_type"
+                            >
+                                <option v-for="u in zendureDeviceTypeList" :key="u.key" :value="u.key">
+                                    {{ u.value }}
+                                </option>
+                            </select>
+                        </div>
+                    </div>
+
+                    <InputElement
+                        :label="$t('batteryadmin.ZendureDeviceSerial')"
+                        v-model="batteryConfigList.zendure_device_serial"
+                        type="text"
+                        minlength="8"
+                        maxlength="8"
+                    />
+
+                    <InputElement
+                        :label="$t('batteryadmin.ZendureMaxOutput')"
+                        v-model="batteryConfigList.zendure_max_output"
+                        type="number"
+                        min="100"
+                        max="2000"
+                        step="100"
+                        :postfix="$t('batteryadmin.Watt')"
+                    />
+
+                    <InputElement
+                        :label="$t('batteryadmin.ZendureMinSoc')"
+                        v-model="batteryConfigList.zendure_soc_min"
+                        type="number"
+                        min="0"
+                        max="60"
+                        step="1"
+                        :postfix="$t('batteryadmin.Percent')"
+                    />
+
+                    <InputElement
+                        :label="$t('batteryadmin.ZendureMaxSoc')"
+                        v-model="batteryConfigList.zendure_soc_max"
+                        type="number"
+                        min="40"
+                        max="100"
+                        step="1"
+                        :postfix="$t('batteryadmin.Percent')"
+                    />
+
+                    <div class="row mb-3">
+                        <label for="zendure_bypass_mode" class="col-sm-2 col-form-label">
+                            {{ $t('batteryadmin.ZendureBypassMode') }}
+                        </label>
+                        <div class="col-sm-10">
+                            <select
+                                id="zendure_bypass_mode"
+                                class="form-select"
+                                v-model="batteryConfigList.zendure_bypass_mode"
+                            >
+                                <option v-for="u in zendureBypassModeList" :key="u.key" :value="u.key">
+                                    {{ $t(`batteryadmin.ZendureBypassMode` + u.value) }}
+                                </option>
+                            </select>
+                        </div>
+                    </div>
+                </CardElement>
+            </template>
+
             <FormFooter @reload="getBatteryConfig" />
         </form>
     </BasePage>
@@ -156,6 +232,7 @@ export default defineComponent({
                 { key: 2, value: 'Mqtt' },
                 { key: 3, value: 'Victron' },
                 { key: 4, value: 'PytesCan' },
+                { key: 5, value: 'ZendureLocalMqtt' },
             ],
             jkBmsInterfaceTypeList: [
                 { key: 0, value: 'Uart' },
@@ -167,6 +244,17 @@ export default defineComponent({
                 { key: 1, value: 'dV' },
                 { key: 0, value: 'V' },
             ],
+            zendureDeviceTypeList: [
+                { key: 0, value: 'Hub 1200' },
+                { key: 1, value: 'Hub 2000' },
+                { key: 2, value: 'AIO 2400' },
+                { key: 3, value: 'Ace 2000' },
+                { key: 4, value: 'Hyper 2000' },
+            ],
+            zendureBypassModeList: [
+                { key: 0, value: 'Automatic' },
+                { key: 1, value: 'AlwaysOff' },
+            ],
         };
     },
     created() {