How to Control Smart Switches for an Off-Grid System to get the Maximum Energy from a PV inverter?
This article discusses theoretical ways to get the maximum energy from a PV inverter by dynamically turning loads on and off, and shows how to practically automate the process in openHAB. The herein presented program logic keeps smart switches relaying electricity as long as there is energy surplus, and also allows manual intervention.
The Requirements and Outlook chapters present technology-independent considerations, the openHAB Setup chapter is focused on implementing the logic.
Discussion at community.openhab.org/t/how-to-control-smart-switches-for-an-off-grid-system-to-get-the-maximum-energy-from-a-pv-inverter/ . Changes of the current text can be seen at mail.aegee.org/cgit/blog/log/content.en/posts/20260908.md.
Requirements
For an off-grid (island) system with photovoltaic inverter I want to turn on devices to utilize the available power, when the sun shines. This can be used e.g. for heating or prolonged cooking. Storing energy in the battery should be avoided, as energy gets lost when stored and extracted from the battery; the lifetime of the battery decreases; and the battery may not have enough capacity to store all produced energy.
In this article relay is a verb: pass or not electricity through; switch means hardware device.
Each switch can be in one of the modes:
- managed: software controls when the electricity flows through the switch. When the switch disconnects from the data network for sporadic reasons and is still connected to the power line, it stops relaying power. When the switch is plugged in the power line, it initially does not relay power.
- manual: the user sets whether power is relayed. When the switch is plugged in the power line, it keeps its last relaying state as before it was unplugged. When the switch loses the connection to the controlling software, nothing changes.
In managed mode the switches are controlled based on the potential of the connected battery. The assumption is that when the battery has more volts, there is more energy surplus. This is not ideal and does not consider how much the sun shines currently. It works good in setups, where the battery is connected with only two cables (plus and minus), there is no communication protocol involved, and the State of Charge (how much percent the battery is full) is calculated solely by the potential of the battery, e.g. 56 Volts always mean 100% charged, while 47 Volts is 0% charged.
In other setups more volts do not mean more State of Charge: The potential increases, while the battery is charged; once the battery is 100% full, its potential decreases; if afterwards the battery is discharged to 99%, it still avoids charging, and then its potential stays relatively low, even when the sun could produce more power than currently consumed.
When sunset approaches, the switches have to be toggled to manual/off mode, so that the remaining energy from the Sun is stored in the battery for the night. In the code below the reader can change the calculate() function to use different criteria to decide whether a switch relays power.
To each modeled switch are assigned minimum and maximum volts: when the battery potential goes below the minimum volts, the switch is instructed to stop relaying power. When the battery potential reaches the maximum volts, the switch starts relaying electricity. When a device is toggled from managed to manual mode, it keeps its on/off relaying state. When a device is toggled from manual to managed mode, and the potential of the battery is between the set minimum and maximum voltage, the device keeps its state. When a device is toggled from manual to managed mode, and the potential of the battery is below the set minimum voltage, or above the set maximum voltage, the electricity is (not) relayed accordingly. When a switch is in managed mode, and it connects to the controlling software, the electricity starts flowing, when the battery voltage is at least the minimum set voltage (this is the purpose of defaultOn below).
The minimum and maximum voltages are per switch, allowing to set priorities where electricity relaying should be turned on/off first. For optimal results the battery potential should be read as often as possible (to detect clouds), e.g. every two seconds. This use case works on low-voltage batteries (46-58 Volts).
The reason for the hysteresis - different volts for on and off - is that once a load starts consuming power, the volts of the battery can immediately decrease and the system needs some time to get in balance: possibly increase the consumption from PV, if previously that power could not go anywhere and was therefore reduced; it can take some time to calculate correctly the battery voltage with the new load when inrush current with motors is involved. While the system gets balanced, the battery voltage can decrease, but nevertheless there might be enough power from PV to serve the load continuously. If there was a single voltage as criterion, instead of voltages for on and off, the load would be turned on and off often and this would not have allowed the inverter to steady increase the consumption from PV, making sun power unused.
The loads behind the switches are unknown and may be zero. When the battery potential decreases, all switches in managed mode are turned off, whose minimal voltage is below the threshold. When the battery potential increases, at most one switch in managed mode is turned on: the one which was off, has the lowest maximum-voltage, and its maximum-voltage is at least the voltage of the battery. In fact, the calculate() function does not differentiate between increasing or decreasing battery voltage, it considers the current battery voltage. If there are more than one switch, which are in managed mode and whose maximum-voltage is at least the current voltage of the battery, when a single switch starts relaying power a timer is started, which executes the calculate() function after 5 seconds. The reason is that if there is no load on the just-turned-on switch, the battery voltage might not change, and if the battery voltage does not change, the calculate() function will not be triggered by change of the battery voltage. At the same time, if more than one switch start relaying electricity at the same time, this could cause too much load at once, reduce the battery voltage fast and soon afterwards lead to stop relaying power for the recently turned on switches. For these reasons, to allow the system to get in balance, at most one switch starts relaying power in a single iteration. When the user changes the minimum or maximum voltage of a device, the calculate() function is executed.
The 5 seconds are arbitrarily chosen on the assumption that during this time, data from the inverter would have been read, and if the battery potential was changed, the calculate() function would have been executed. If data from the inverter was read, and the battery potential has not changed, then execute calculate() again in order to turn on another switch.
openHAB Setup
Bindings
The add-ons used are MQTT binding, JSONPATH transformations, MAPDB persistence and Java223 automation. The latter is available only in the market place and allows seamless running of openHAB 5.2 on RPi 3B system in 32-bit mode. In the Java223 add-on settings the “☐ Enable Helper Library” preference can be switched off - the functionality is not required by this example.
The switches and openHAB communicate over MQTT. For this example there must exist an MQTT broker (bridge) in openHAB with UID mqtt:broker:b. With the default Quality of Service being 0 messages can get lost.
Configuration on the Switch Device Web Interface
This example uses Shelly Plus PM1 smart switches.
The switches are named r1 and r2. The logic here allows having more switches: r3, r4…
Over the web interface of Shelly the names are set under ⛭Settings → MQTT → MQTT Prefix. At the same page is also checked “☑Generic status update over MQTT”. Since firmware 0.14.0 the parameter “☑Enable RPC over MQTT” is shown, it must be checked too, but all is good if this parameter is not available. The “Client ID” of each client connecting to an MQTT server must be distinct. Beware that after every change on the webpage, the password must be retyped before clicking “Save Settings”.
Under ⛭Settings → Device Settings → Eco mode is checked “☑Enable Eco mode”. This is not required and in fact with or without eco mode no difference was observed.
Apart from setting up the MQTT connection, this script is created on each device under “<> Scripts”:
MQTT.setDisconnectHandler(function() {
if (Shelly.getComponentConfig('switch', 0).initial_state == "off")
Shelly.call('Switch.Set', {id: 0, on: false})
})The above script must be “enabled”, “Run at startup” - executed whenever the switch is plugged in the power line. Its purpose is to stop relaying electricity, when at the same time the connection to the MQTT server and thus to openHAB is lost, and openHAB controls whether the device relays power. (initial_state == "restore_last" would mean that openHAB or any other means can still turn the relaying on and off, but openHAB does not use the logic below for managed mode to do so.)
The MQTT Things
Thing mqtt:topic:r1 "Relay 1" (mqtt:broker:b) [availabilityTopic="r1/online", payloadNotAvailable="false", payloadAvailable="true"] {
Channels:
Type switch : onoff "Relay 1" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.output", off="false", on="true", formatBeforePublish="{\"src\":\"u\",\"method\":\"Switch.Set\",params:{\"id\":0,\"on\":%s}}", commandTopic="r1/rpc"]
Type switch : mode "Relay 1 Mode" [stateTopic="r1V/rpc", transformationPattern="JSONPATH:$.result.initial_state", off="off", on="restore_last"] // on: manual mode, off: managed mode
Type number : current "Relay 1 Current" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.current", unit="A"]
Type contact : online "Relay 1 Online" [stateTopic="r1/online", on="true", off="false"]
Type number : energy "Relay 1 Energy" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.aenergy.total", unit="Wh"]
Type number : power "Relay 1 Power" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.apower", unit="VA"]
Type number : temperature "Relay 1 Temperature" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.temperature.tC", unit="°C"]
Type number : voltage "Relay 1 Potential" [stateTopic="r1/status/switch:0", transformationPattern="JSONPATH:$.voltage", unit="V"]
}Likewise for r2, r3…
The Items
Number:ElectricPotential batteryPotential "🔋Battery Potential" [Measurement, Voltage]
Group gRelay1 "Relay 1" <poweroutlet> [PowerOutlet]
Number:ElectricPotential r1_on "Relay 1 Turn on[%.1f %unit%]" (gRelay1) [Control, Voltage]
Number:ElectricPotential r1_off "Relay 1 Turn off[%.1f %unit%]" (gRelay1) [Control, Voltage]
Switch r1_mode "Relay 1 Mode[%s]" <f7:gear> (gRelay1) [Switch] {channel="mqtt:topic:r1:mode", stateDescription=""[readOnly=false], autoupdate="false"}
Switch r1_onoff "Relay 1 State[%s]" <switch> (gRelay1) [Switch, Power] {channel="mqtt:topic:r1:onoff", autoupdate="false"}
Contact r1_online "Relay 1 Connected" (gRelay1) [Status, OpenState] {channel="mqtt:topic:r1:online"}
Number:Energy r1_energy "Relay 1 Energy" (gRelay1) [Measurement, Energy] {channel="mqtt:topic:r1:energy", unit="Wh"}
Number:ElectricCurrent r1_current "Relay 1 Current[%.3f %unit%]" (gRelay1) [Measurement, Current] {channel="mqtt:topic:r1:current"}
Number:Power r1_power "Relay 1 Power[%.1f %unit%]" <poweroutlet> (gRelay1) [Measurement, Power] {channel="mqtt:topic:r1:power", unit="VA"}
Number:Temperature r1_temperature "Relay 1 Temperature" <temperature> (gRelay1) [Measurement, Temperature] {channel="mqtt:topic:r1:temperature"}
Number:ElectricPotential r1_voltage "Relay 1 Potential[%.1f %unit%]" (gRelay1) [Measurement, Voltage] {channel="mqtt:topic:r1:voltage"}
String r1_color (gRelay1)Likewise for r2.
MapDB Persistence Configuration
Create the file openhab/persistence/mapdb.persist:
Items {
r1_on, r1_off, r2_on, r2_off: strategy=everyChange, restoreOnStartup
}The above stores for each switch the minimum and maximum voltage, which controls when the switch relays electricity, depending on the battery potential.
The Rules
While there are many openHAB rules, all of which can change the managedRelays list, for simplicity there are no synchronizations from the Java language used to avoid multithreading conflicts. In practice no problems were observed.
When a device loses the connection to openHAB, the value of the items - current, energy, mode, onoff (whether electricity relaying is permitted/blocked), power, temperature, voltage - are set to UnDefType.NULL.
The purpose of color is expained in the next section The Sitemap.
openhab/automation/jsr223/SmartRelays.java:
import java.lang.reflect.*;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Stream;
import org.openhab.automation.java223.common.InjectBinding;
import org.openhab.core.automation.Action;
import org.openhab.core.automation.module.script.action.*;
import org.openhab.core.automation.module.script.defaultscope.ScriptThingActions;
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule;
import org.openhab.core.automation.util.TriggerBuilder;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.items.*;
import org.openhab.core.items.events.*;
import org.openhab.core.library.items.*;
import org.openhab.core.library.types.*;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.types.*;
public class SmartRelays {
@InjectBinding(preset = "RuleSupport") ScriptedAutomationManager automationManager;
@InjectBinding(preset = "ScriptAction") ScriptExecution scriptExecution;
ItemRegistry ir;
/** The item containing the volts of the battery */
NumberItem volts;
BusEvent events;
org.openhab.core.automation.module.script.action.Timer timer;
ScriptThingActions actions;
/** All utilized relays */
Relay[] relays;
/** Relays which are both online and in managed mode */
List<Relay> managedRelays;
ThingRegistry things;
/** Models a Shelly Plus1PM smart relay with connected items */
class Relay implements Comparable<Relay> {
String name;
Thing thing;
SwitchItem item;
/** {@link OnOffType#ON} - manual mode; {@link OnOffType#OFF} - managed mode */
SwitchItem mode;
/** When the battery has these voltages or more this relay is turned on in managed mode */
NumberItem max;
/** When the battery has less than these voltages this relay is turned off in managed mode */
NumberItem min;
/**
* Creates a new Relay object from a prefix. Items with predefined names after the prefix must exist. The prefix must be exactly two characters.
* @param prefix The base of all the item names linked to the relay.
*/
Relay(String prefix) {
name = prefix;
thing = things.get(new ThingUID("mqtt:topic:" + prefix));
mode = (SwitchItem)ir.get(prefix + "_mode");
item = (SwitchItem)ir.get(prefix + "_onoff");
max = (NumberItem)ir.get(prefix + "_on");
min = (NumberItem)ir.get(prefix + "_off");
}
/**
* @return whether the linked hardware thing is connected to MQTT
*/
boolean isOnline() {
return thing.getStatus() == ThingStatus.ONLINE;
}
void setColor() {
events.postUpdate(name + "_color", switch (mode.getState().toString() + item.getState().toString()) {
case "ONON" -> "red";
case "ONOFF" -> "maroon";
case "OFFON" -> "lime";
case "OFFOFF" -> "green";
default -> "";
});
}
/**
* Turns the relaying of electricity on or off.
* @param state whether the relay should relay or block power
*/
void turn(OnOffType state) {
item.send(state);
}
/** Used for sorting the relays, so that earlier positioned relays are turned on sooner in managed mode. */
@Override public int compareTo(Relay r) {
return Float.compare(max.getStateAs(DecimalType.class).floatValue(), r.max.getStateAs(DecimalType.class).floatValue());
}
}
/**
* Publishes a value to a topic on MQTT
* @see <a href="https://www.openhab.org/addons/bindings/mqtt/#rule-actions">MQTT binding: Rule Actions</a>
* @param topic the topic
* @param value the value
*/
void publishMQTT(String topic, String value) {
try {
ThingActions mqttBroker = actions.get("mqtt", "mqtt:broker:b");
Method publishMQTT = mqttBroker.getClass().getMethod("publishMQTT", String.class, String.class);
publishMQTT.invoke(mqttBroker, topic, value);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {}
}
Relay find(String s) {
for (Relay r: relays) if (r.name.equals(s)) return r;
return null;
}
/**
* Sets {@link managedRelays} to contain all relays, which are both online and in managed mode.
*/
void updateManagedRelays() {
managedRelays = Arrays.stream(relays).filter(e -> e.isOnline() && e.mode.getState() == OnOffType.OFF).toList();
}
/**
* Evaluates which relays in managed mode to turn on and off based on the battery potential and per relay preferences for minimum and maximum voltage.
*
* @param data contains at most one key. If the key is {@code defaultOn} then the relay contained in the entry just went online: if this relay is in managed mode and its {@code min} voltage is at least the potential of the battery, then turn it on. Otherwise relays are turned on, only after the battery voltage goes above the {@code max} voltage of the relay. If the key is {@code itemName}, then in a previous iteration another relay was turned on and for this reason this particular one was skipped. If the relay behind the key {@code itemName} is currently open, then {@code calculate()} was run in the meantime, thus do nothing.
*/
void calculate(Map<String, Relay> data) {
if (timer != null) timer.cancel();
if (data != null && managedRelays.contains(data.get("itemName")) && data.get("itemName").item.getState() == OnOffType.ON) return;
float vol = volts.getState() instanceof Number n ? n.floatValue() : 0;
boolean quit = false;
for (Relay r: managedRelays) {
if (r.item.getState() == OnOffType.ON && r.min.getStateAs(DecimalType.class).floatValue() > vol) {
r.turn(OnOffType.OFF);
quit = true;
}
}
if (quit) return;
Relay defaultOn = data != null ? data.get("defaultOn") : null;
Relay[] onManagedRelays = managedRelays.stream().filter(e -> e.item.getState() != OnOffType.ON && (vol >= e.max.getStateAs(DecimalType.class).floatValue() || (e == defaultOn && vol >= e.min.getStateAs(DecimalType.class).floatValue()))).sorted().limit(2).toArray(Relay[]::new);
if (onManagedRelays.length > 0) {
onManagedRelays[0].turn(OnOffType.ON);
if (onManagedRelays.length > 1)
timer = scriptExecution.createTimer(ZonedDateTime.now().plusSeconds(5),
() -> calculate(Map.of("itemName", onManagedRelays[1])));
}
}
/**
* The method is invoked by openHAB via the JSR223 interface when the current file is loaded.
* @see <a href="https://www.openhab.org/docs/configuration/jsr223.html#scriptloaded-and-scriptunloaded-functions">JSR223 in openHAB: {@code scriptLoaded} and {@code scriptUnloaded} functions</a>
* @param filename The filename from which the program is loaded.
*/
public void scriptLoaded(String filename) {
volts = (NumberItem)ir.get("batteryPotential");
while (true) {
Thing thing = things.get(new ThingUID("mqtt:broker:b"));
if (thing != null && thing.getStatus() == ThingStatus.ONLINE) break;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
break;
}
}
// java.lang.reflect.InvocationTargetException is thrown if the Relay constructor fails
relays = Stream.of("r1", "r2").map(e -> new Relay(e)).toArray(Relay[]::new);
updateManagedRelays();
automationManager.addRule(new SimpleRule() {
{
name = "Update volts for On/Off";
uid = "volts_on_off";
description = "Ensures that the battery volts for turning off a relay are less than the volts for turning the relay on";
triggers = Arrays.stream(relays).flatMap(it -> Stream.of(
TriggerBuilder.create().withId("Off_" + it.name).withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(new Configuration(Map.of("itemName", it.name + "_off"))).build(),
TriggerBuilder.create().withId("On_" + it.name).withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(new Configuration(Map.of("itemName", it.name + "_on"))).build()
)).toList();
}
@Override public Object execute(Action module, Map<String, ?> inputs) {
String i = ((ItemEvent)inputs.get("event")).getItemName(), y = i.substring(0, 2);
Relay r = find(y);
if (i.endsWith("_on")) {
if (r.max.getStateAs(DecimalType.class).floatValue() - r.min.getStateAs(DecimalType.class).floatValue() < 0.1)
r.min.send(new DecimalType(r.max.getStateAs(DecimalType.class).floatValue() - 0.1));
} else if (r.max.getStateAs(DecimalType.class).floatValue() - r.min.getStateAs(DecimalType.class).floatValue() < 0.1)
r.max.send(new DecimalType(r.min.getStateAs(DecimalType.class).floatValue() + 0.1));
calculate(null);
return null;
}
});
automationManager.addRule(new SimpleRule() {
{
name = "Managed Control";
uid = "managed-control";
description = "Evaluates whether to toggle the managed relays anytime the potential of the battery changes";
triggers = List.of(
TriggerBuilder.create().withId("t").withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(new Configuration(Map.of("itemName", "batteryPotential"))).build()
);
}
@Override public Object execute(Action module, Map<String, ?> inputs) {
calculate(null);
return null;
}
});
automationManager.addRule(new SimpleRule() {
{
name = "Toggle between manual/managed mode when the rX_mode item is altered";
uid = "switch_managed_manual_mode";
triggers = Arrays.stream(relays).map(e ->
TriggerBuilder.create().withId(e.name).withTypeUID("core.ItemCommandTrigger")
.withConfiguration(new Configuration(Map.of("itemName", e.name + "_mode"))).build()
).toList();
}
@Override public Object execute(Action module, Map<String, ?> inputs) {
ItemCommandEvent ie = (ItemCommandEvent)inputs.get("event");
String y = ie.getItemName().substring(0, 2), t = y + "/rpc";
publishMQTT(t, "{\"src\":\"U\",\"method\":\"Switch.SetConfig\",\"params\":{\"id\":0,\"config\":{\"initial_state\":\"" + ((OnOffType)ie.getItemCommand() == OnOffType.ON ? "restore_last" : "off") + "\"}}}");
publishMQTT(t, "{\"id\":2,\"src\":\"" + y + "V\",\"method\":\"Switch.GetConfig\",\"params\":{\"id\":0}}");
return null;
}
});
automationManager.addRule(new SimpleRule() {
{
name = "Turn a relay on or off by changing the mode or explicitly in manual mode";
uid = "poweron_off";
triggers = Arrays.stream(relays).flatMap(it -> Stream.of(
TriggerBuilder.create().withId("t" + it.name).withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(new Configuration(Map.of("itemName", it.name + "_mode"))).build(),
TriggerBuilder.create().withId("u" + it.name).withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(new Configuration(Map.of("itemName", it.name + "_onoff"))).build()
)).toList();
}
@Override public Object execute(Action module, Map<String, ?> inputs) {
String y = ((ItemEvent)inputs.get("event")).getItemName().substring(0, 2);
Relay r = find(y);
r.setColor();
if (((String)inputs.get("module")).charAt(0) == 't') {
updateManagedRelays();
calculate(inputs.get("oldState") == UnDefType.NULL ? Map.of("defaultOn", r) : null);
}
return null;
}
});
automationManager.addRule(new SimpleRule() {
{
name = "Set all connected items to NULL on disconnect";
uid = "nulling";
description = "NULLify all properties on disconnect, fetch mode on connect, update managedRelays list";
triggers = Arrays.stream(relays).map(e ->
TriggerBuilder.create().withId(e.name).withTypeUID("core.ThingStatusChangeTrigger")
.withConfiguration(new Configuration(Map.of("thingUID", "mqtt:topic:" + e.name))).build()
).toList();
}
@Override public Object execute(Action module, Map<String, ?> inputs) {
updateManagedRelays();
if (inputs.get("newStatus") == ThingStatus.OFFLINE) {
String y = inputs.get("module") + "_";
for (String s: new String[] {"current", "energy", "mode", "onoff", "power", "temperature", "voltage"})
events.postUpdate(y + s, "NULL");
} else
publishMQTT((String)inputs.get("module") + "/rpc", "{\"id\":3,\"src\":\"" + (String)inputs.get("module") + "V\",\"method\":\"Switch.GetConfig\",\"params\":{\"id\":0}}");
return null;
}
});
for (Relay r: relays)
if (r.isOnline())
publishMQTT(r.name + "/rpc", "{\"id\":4,\"src\":\"" + r.name + "V\",\"method\":\"Switch.GetConfig\",\"params\":{\"id\":0}}");
calculate(null);
}
/**
* Invoked when the current file is unloaded.
* @see <a href="https://www.openhab.org/docs/configuration/jsr223.html#scriptloaded-and-scriptunloaded-functions">JSR223 in openHAB: {@code scriptLoaded} and {@code scriptUnloaded} functions</a>
*/
public void scriptUnloaded() {
if (timer != null) timer.cancel();
}
}The Sitemap
The switches are shown in a sitemap. They are hidden from the sitemap, when the device loses the connection to openHAB. The sitemap uses hierarchical presentation with { … }. At the top level is shown the name of the switch and the colored used power. The color of the value is used to signal four states: lime (light green) - managed mode, power is relayed; green - managed mode, relaying power is prohibited; red - manual mode, electricity is relayed; maroon (dark red) - manual mode, power is not relayed.
On openHAB-Android version 3.21.0-beta Switch mappings=[…] does enact the autoupdate=false property in this way: when the user clicks on the new desired state, openHAB sends a command over MQTT to the device, the device replies with its new state and then openHAB updates the manual/managed mode. In other words, if the user presses to toggle the mode and the device is disconnected, the user does not see changes.
The status of the switch is presented by two elements. The first element shows if relaying power is allowed or blocked. The other element shows if the mode is manual or managed. In manual mode the first element can be toggled by the user.
As snippet in a sitemap, likewise for r2:
Text item=batteryPotential
Text item=r1_power visibility=[r1_online OPEN] valuecolor=[r1_color "green"="green", r1_color "lime"="lime", r1_color "red"="red", r1_color "maroon"="maroon"] {
Switch item=r1_onoff visibility=[r1_mode != OFF]
Text item=r1_onoff visibility=[r1_mode OFF] valuecolor=[ON="green", OFF="red"]
Switch item=r1_mode visibility=[!=NULL] mappings=[ON="Manual", OFF="Managed"] labelcolor=[ON="red", OFF="green"]
Text item=r1_voltage
Text item=r1_current
Text item=r1_temperature
Text item=r1_energy
Setpoint item=r1_on visibility=[r1_mode OFF] minValue=49.8 maxValue=55 step=0.1
Setpoint item=r1_off visibility=[r1_mode OFF] minValue=49.3 maxValue=53.5 step=0.1
}In the above sitemap toggling a switch to (not) relay power may require a single click: change the mode from manual to managed; or in manual mode pressing the on/off toggle. But sometimes two clicks are necessary: change the mode to manual and then press the on/off toggle. The amount of needed clicks could be reduced by having one element with three states: Managed, On, Off. The latter two imply manual mode. To show in managed mode whether the switch relays electricity, the labelcolor of the three-state element can be set accordingly, or an additional element appears, displaying if the switch is opened or closed.
Last but not least, before running the rules, set for each switch the potential of the battery, for which relaying power should be enabled or disabled in managed mode.
Outlook
The herein presented setup does very much enable the maximum utilization of available power from photovoltaic off-grid inverters. Yet it is not perfect: the minimum and maximum volts per switch need regular adjustments, based on the current sunshine, priorities of the user, and attached load, in order to get it on the edge, where turning on and off a load happens frequently.
A disadvantage of this algorithm is, that the current sunshine is not considered, it cannot be calculated when nothing uses power, how much load can in addition be added, so that the PV power production will increase. If an inverter does not expose this information directly, how can it be determined based on the data the inverter provides?
In some setups the potential of the low-voltage battery is slightly less than the potential of the PV panels. However when the battery has no more capacity to take energy, the potential of the battery stays at approximately 53V, while the potential of the PV panels increases to 65-90V. This could be used as a criterion, that the energy consumption should increase.
Another criterion if there is energy surplus, so devices should be turned on, can be when the battery is filled with a lot of amperes. Disadvantage is, that when the battery is full, the current decreases to zero, and then this criterion does not apply.
The one million coins question is, how to calculate how much load to create (attach, turn on), so that the power produced from the PV panels matches closely the power consumed from the loads on the inverter, leading to zero current in and out to the battery.
Ideally inverters should expose a number: how much AC power should be used in this very moment, so that the not reduced PV input is equal to the AC load.