# Smart battery with Zendure, Tibber and NodeRed

I have an 820W balcony solar system, feeding into a [Zendure SolarFlow](https://zendure.de/products/solarflow) with two 960Wh batteries and then going through a Hoymiles HM-800 into my home.

My goal was to store the solar energy in the batteries, and if the energy prices are higher, discharge into my home.

### The Problem

There is no solution to control it locally, only with the app. Zendure only has a public MQTT in the internet that is not locally available, an one can only read data from it. It looked like I needed to wait until Zendure would give me a method to control the device locally.

Man, I already have the energy prices, the current energy usage of my home, even the current solar energy and the battery percentage of my Zendure system... there must be a way. And there is!

You remember my HM-800? Instead of the Hoymiles DTU, I am using a [AhoyDTU](https://ahoydtu.de/). With it, I can send a limit to the Inverter. I did already set a max output of 300W in the Zendure App (because thats a little bit under the base energy usage of my home) and now I set the Inverter limit to 0%. Now the Zendure system moves all solar energy into the battery. If the limit is reverted to 100%, it moves energy into the home. Nice!

Because the AhoyDTU has MQTT, and one can set a limit, my plan was almost complete. If there only was a way to automate this...

### The Solution

My current setup uses a [NodeRed](https://nodered.org/) instance in [HomeAssistant](https://www.home-assistant.io/), an [AhoyDTU](https://ahoydtu.de/), the [Tibber API](https://developer.tibber.com/) and the public [Zendure MQTT](https://github.com/Zendure/developer-device-data-report/).

Every hour, I get the current prices from Tibber, store them, wait 5 seconds, read the stored prices, calculate the average price and store it in an HomeAssistant Sensor named "elecricity\_price\_average".

Then, every time the Tibber Integration changes the price, or the battery percentage changes (I get the value from the Zendure MQTT and store it in its own sensor), I get the values with a handy "ValueGetter"-Subflow, check if it should discharge into my home (which it should when the current price is higher than the average price of the day), store the limit in a sensor and send the new limit to the inverter with MQTT.

This is my final flow:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711447440676/42a50c1c-815b-473d-a97f-46e08660032a.png align="center")

### Code

Average price calculation:

```javascript
function calculateAverage(values) {
    // Check if the values array is not empty
    if (values.length === 0) {
        return 0;
    }

    // Calculate the sum of all values
    var sum = values.reduce(function(a, b) {
        return a + b;
    });

    // Calculate the average
    var average = sum / values.length;
    
    return average;
}

let prices = [];
// Push all prices into an array
msg.payload.forEach(function(price) {
    prices.push(price.total);
});

// the 0.00 is for an offset that can be added to the average
let result = Math.round((calculateAverage(prices) + 0.00) * 1000) / 1000

return msg = {
    "payload": result
};
```

Discharge condition:

```javascript
// Prepare values
let battery = msg.payload.battery;
let price = msg.payload.price;
let average = msg.payload.averagePrice;
let solar = msg.payload.solarEnergy;

// Charge battery
msg.payload = "0"

// Price high -> discharge battery
if (price > average) {
    msg.payload = "100"
}

if (
    (battery <= 20 && solar > 0) ||
    (battery <= 12 && solar == 0)
) {
    // Charge battery
    msg.payload = "0"
}

// Battery full -> excess solar into home
if (battery >= 100 && solar > 0) {
    msg.payload = "100"
}

// Send new limit to inverter
return msg;
```

MQTT topic:

```plaintext
ahoydtu/ctrl/limit/0
```

ValueGetter Subflow:

```json
[{"id":"f193ca922ddcefac","type":"subflow","name":"ValueGetter","info":"","category":"Vratny","in":[{"x":120,"y":260,"wires":[{"id":"c56d509f669595a3"}]}],"out":[{"x":1180,"y":260,"wires":[{"id":"8e25b00de8b09d79","port":0}]}],"env":[{"name":"entities","type":"json","value":"{}","ui":{"icon":"font-awesome/fa-bars","label":{"de":"Entitäten"}}},{"name":"topic","type":"str","value":""}],"meta":{},"color":"#3FADB5","icon":"node-red/join.svg"},{"id":"b4a9ab518ebd88d2","type":"api-current-state","z":"f193ca922ddcefac","name":"","server":"4db50fc5.1329b","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":720,"y":260,"wires":[["b1f94791af70a145"]]},{"id":"b1f94791af70a145","type":"join","z":"f193ca922ddcefac","name":"Zusammenführen","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":910,"y":260,"wires":[["8e25b00de8b09d79"]]},{"id":"8e25b00de8b09d79","type":"function","z":"f193ca922ddcefac","name":"Filter","func":"let newMsg={};\nnewMsg.payload=msg.payload;\nnewMsg.topic = env.get(\"topic\");\nreturn newMsg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1070,"y":260,"wires":[[]]},{"id":"989cce5990120f96","type":"split","z":"f193ca922ddcefac","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"topic","x":390,"y":260,"wires":[["90c052159aac0c60"]]},{"id":"90c052159aac0c60","type":"function","z":"f193ca922ddcefac","name":"Convert","func":"msg.payload = { 'entityId': msg.payload};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":260,"wires":[["b4a9ab518ebd88d2"]]},{"id":"c56d509f669595a3","type":"function","z":"f193ca922ddcefac","name":"Get Variable","func":"msg.payload=env.get(\"entities\");\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":230,"y":260,"wires":[["989cce5990120f96"]]},{"id":"4db50fc5.1329b","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"","statusSeparator":"","enableGlobalContextStore":false}]
```

ValueGetter Entities:

```json
{
    "solarEnergy": "sensor.solarenergie",
    "battery": "sensor.batterie_ladung",
    "price": "sensor.electricity_price_your_address",
    "averagePrice": "sensor.electricity_price_average"
}
```
