The new Opportunity Loads feature allows to run any kind of consumer based on available solar surplus and hot water heating is one of the most common use cases to burn “unusable” surplus.
With a quite simple Node-RED script, control of these consumers can be enabled/disabled based on conditions, i.e. a reached target-temperature or eventually weekday specific goals.
Example1: 3x1kW heating rod for general hot-water production.
We have a regular gas-heating for hot-water purpose, at current gas-prices / feed in prices it is still reasonable to use surplus power to make hot water over feeding in. Yet, heating the reservoir up above 50° is undesired, it causes increasing heat-losses, thermal stress, higher ambient temperature in the basement and is no “real saving”, because 50° is where we limit the gas-based-heating, and hence there are no more “alternative heating costs” that could be considered as saving beyond that point.
The node reads a already known temperature sensor from the system and uses three different pairs of thresholds (44/45, 46/47, 49/50) to change the control behaviour on individual heating rods. I’ve used a delta of 1° (i.e. OFF@45°, AUTO@44°) to avoid flickering on edge cases:
The Node does:
-
Whenever a threshold is reached: Set the
/AutoPath of the corresponding shelly to0and send a 5 second delayed0to/Stateas well - as that shelly now just switched to manual mode, and should be ensured to be “off”. Opportunity Loads will now see aNO_CTRLstate and not assign any energy to these devices.
-
Whenever the activation threshold is reached, it simply sets
/Autoto1again, making the shelly offer a controllable state to Opportunity Loads again - and Opportunity Loads will resume to orchestrate it along with any other existing consumers.
JSON of the Node
[
{
"id": "9aecdc9c7c2fb5ae",
"type": "tab",
"label": "OL:Heater",
"disabled": false,
"info": "",
"env": []
},
{
"id": "28c61cd4e6c5ee07",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/54",
"path": "/SwitchableOutput/1/Auto",
"serviceObj": {
"service": "com.victronenergy.acload/54",
"name": "Heater1 (54)"
},
"pathObj": {
"path": "/SwitchableOutput/1/Auto",
"name": "/SwitchableOutput/1/Auto",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 40,
"wires": []
},
{
"id": "1bbb71b0617cd912",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/55",
"path": "/SwitchableOutput/0/Auto",
"serviceObj": {
"service": "com.victronenergy.acload/55",
"name": "Heater2 (55)"
},
"pathObj": {
"path": "/SwitchableOutput/0/Auto",
"name": "/SwitchableOutput/0/Auto",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 180,
"wires": []
},
{
"id": "af6af2c5ad9c10ae",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/56",
"path": "/SwitchableOutput/1/Auto",
"serviceObj": {
"service": "com.victronenergy.acload/56",
"name": "Heater3 (56)"
},
"pathObj": {
"path": "/SwitchableOutput/1/Auto",
"name": "/SwitchableOutput/1/Auto",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 320,
"wires": []
},
{
"id": "ae6cf2a1cb695052",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/54",
"path": "/SwitchableOutput/1/State",
"serviceObj": {
"service": "com.victronenergy.acload/54",
"name": "Heater1 (54)"
},
"pathObj": {
"path": "/SwitchableOutput/1/State",
"name": "/SwitchableOutput/1/State",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 100,
"wires": []
},
{
"id": "a72b225cbd996c27",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/55",
"path": "/SwitchableOutput/0/State",
"serviceObj": {
"service": "com.victronenergy.acload/55",
"name": "Heater2 (55)"
},
"pathObj": {
"path": "/SwitchableOutput/0/State",
"name": "/SwitchableOutput/0/State",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 240,
"wires": []
},
{
"id": "0b720246c14402e5",
"type": "victron-output-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.acload/56",
"path": "/SwitchableOutput/1/State",
"serviceObj": {
"service": "com.victronenergy.acload/56",
"name": "Heater3 (56)"
},
"pathObj": {
"path": "/SwitchableOutput/1/State",
"name": "/SwitchableOutput/1/State",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 380,
"wires": []
},
{
"id": "185bf6af606be86b",
"type": "delay",
"z": "9aecdc9c7c2fb5ae",
"name": "delay",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 790,
"y": 380,
"wires": [
[
"0b720246c14402e5"
]
]
},
{
"id": "4923510d2121a762",
"type": "switch",
"z": "9aecdc9c7c2fb5ae",
"name": "Threshold 45°",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "44",
"vt": "num"
},
{
"t": "gte",
"v": "45",
"vt": "num"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 440,
"y": 360,
"wires": [
[
"93912373fe01f375"
],
[
"7eca412d9a9ec5e9"
]
]
},
{
"id": "93912373fe01f375",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set On",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 620,
"y": 320,
"wires": [
[
"af6af2c5ad9c10ae"
]
]
},
{
"id": "7eca412d9a9ec5e9",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set Off",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 610,
"y": 380,
"wires": [
[
"af6af2c5ad9c10ae",
"185bf6af606be86b"
]
]
},
{
"id": "69b36403075158b7",
"type": "delay",
"z": "9aecdc9c7c2fb5ae",
"name": "delay",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 790,
"y": 240,
"wires": [
[
"a72b225cbd996c27"
]
]
},
{
"id": "06259ede14142a93",
"type": "switch",
"z": "9aecdc9c7c2fb5ae",
"name": "Threshold 47",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "46",
"vt": "num"
},
{
"t": "gte",
"v": "47",
"vt": "num"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 430,
"y": 220,
"wires": [
[
"43db3fb3b0d96050"
],
[
"2004bb63ce190905"
]
]
},
{
"id": "43db3fb3b0d96050",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set On",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 620,
"y": 180,
"wires": [
[
"1bbb71b0617cd912"
]
]
},
{
"id": "2004bb63ce190905",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set Off",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 610,
"y": 240,
"wires": [
[
"69b36403075158b7",
"1bbb71b0617cd912"
]
]
},
{
"id": "960303af45c79566",
"type": "delay",
"z": "9aecdc9c7c2fb5ae",
"name": "delay",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 790,
"y": 100,
"wires": [
[
"ae6cf2a1cb695052"
]
]
},
{
"id": "2a0925e8aab5332a",
"type": "switch",
"z": "9aecdc9c7c2fb5ae",
"name": "Threshold 50°",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "49",
"vt": "num"
},
{
"t": "gte",
"v": "50",
"vt": "num"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 440,
"y": 80,
"wires": [
[
"2e67f28bcc803e65"
],
[
"6dea9d150da8ec76"
]
]
},
{
"id": "2e67f28bcc803e65",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set On",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 620,
"y": 40,
"wires": [
[
"28c61cd4e6c5ee07"
]
]
},
{
"id": "6dea9d150da8ec76",
"type": "change",
"z": "9aecdc9c7c2fb5ae",
"name": "Set Off",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 610,
"y": 100,
"wires": [
[
"960303af45c79566",
"28c61cd4e6c5ee07"
]
]
},
{
"id": "da4ba9ad6066d2d9",
"type": "victron-input-custom",
"z": "9aecdc9c7c2fb5ae",
"service": "com.victronenergy.temperature/100",
"path": "/Temperature",
"serviceObj": {
"service": "com.victronenergy.temperature/100",
"name": "Reservoir (100)"
},
"pathObj": {
"path": "/Temperature",
"name": "/Temperature",
"type": "number",
"value": 54.3
},
"name": "",
"onlyChanges": true,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"outputTrue": "true",
"outputFalse": "false",
"debounce": "2000",
"x": 160,
"y": 220,
"wires": [
[
"06259ede14142a93",
"4923510d2121a762",
"2a0925e8aab5332a"
]
]
},
{
"id": "4aad3a7092694ff6",
"type": "global-config",
"env": [],
"modules": {
"@victronenergy/node-red-contrib-victron": "1.7.13"
}
}
]
Example 2: 700W Heat pump + 2.7kW Heater for the Kids Pool
For the kids, we have a pool, and our feel-well temperature is somewhere around 28-30° Thus, Opportunity Loads is also orchestrating 2 shellies, one connected to a Heat pump (more efficient), one connected to a regular heating rod, used for “burst-heating”
The flow is literally the same as example 1, just a different reasoning as this is not about “saving and opportunistic costs”, but just about getting the pool temperature about right: Up to 28° it should run full-throttle, whenever there is solar power available. 28-30 it can omit the burst-heater and only rely on the more efficient heat pump to reach the final 30° target eventually.
(Yes, 3 days of cold weather, we are far away from that
)
JSON of the Node
[
{
"id": "a970ae66d296654b",
"type": "tab",
"label": "OL: Pool Heating",
"disabled": false,
"info": "",
"env": []
},
{
"id": "4b6e50f4505e23d0",
"type": "victron-input-custom",
"z": "a970ae66d296654b",
"service": "com.victronenergy.temperature/101",
"path": "/Temperature",
"serviceObj": {
"service": "com.victronenergy.temperature/101",
"name": "Pool (101)"
},
"pathObj": {
"path": "/Temperature",
"name": "/Temperature",
"type": "number",
"value": 25.84
},
"name": "",
"onlyChanges": true,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"outputTrue": "true",
"outputFalse": "false",
"debounce": "2000",
"x": 170,
"y": 180,
"wires": [
[
"83fb8e2ae1be3c86",
"e83d1380812b63d4"
]
]
},
{
"id": "7e5ec9c8ff2d9227",
"type": "victron-output-custom",
"z": "a970ae66d296654b",
"service": "com.victronenergy.acload/57",
"path": "/SwitchableOutput/0/Auto",
"serviceObj": {
"service": "com.victronenergy.acload/57",
"name": "Pool-Heatpump (57)"
},
"pathObj": {
"path": "/SwitchableOutput/0/Auto",
"name": "/SwitchableOutput/0/Auto",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 60,
"wires": []
},
{
"id": "9282cc55b6e5d99f",
"type": "victron-output-custom",
"z": "a970ae66d296654b",
"service": "com.victronenergy.acload/57",
"path": "/SwitchableOutput/0/State",
"serviceObj": {
"service": "com.victronenergy.acload/57",
"name": "Pool-Heatpump (57)"
},
"pathObj": {
"path": "/SwitchableOutput/0/State",
"name": "/SwitchableOutput/0/State",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1020,
"y": 120,
"wires": []
},
{
"id": "225a45b98256fe30",
"type": "delay",
"z": "a970ae66d296654b",
"name": "delay",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 770,
"y": 120,
"wires": [
[
"9282cc55b6e5d99f"
]
]
},
{
"id": "83fb8e2ae1be3c86",
"type": "switch",
"z": "a970ae66d296654b",
"name": "Threshold 30°",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "29",
"vt": "num"
},
{
"t": "gte",
"v": "30",
"vt": "num"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 420,
"y": 100,
"wires": [
[
"38f2b6c0a599b525"
],
[
"792cf63227c22c8e"
]
]
},
{
"id": "38f2b6c0a599b525",
"type": "change",
"z": "a970ae66d296654b",
"name": "Set On",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 600,
"y": 60,
"wires": [
[
"7e5ec9c8ff2d9227"
]
]
},
{
"id": "792cf63227c22c8e",
"type": "change",
"z": "a970ae66d296654b",
"name": "Set Off",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 590,
"y": 120,
"wires": [
[
"225a45b98256fe30",
"7e5ec9c8ff2d9227"
]
]
},
{
"id": "d730adf8e69094ed",
"type": "victron-output-custom",
"z": "a970ae66d296654b",
"service": "com.victronenergy.acload/58",
"path": "/SwitchableOutput/1/Auto",
"serviceObj": {
"service": "com.victronenergy.acload/58",
"name": "Pool-Heater (58)"
},
"pathObj": {
"path": "/SwitchableOutput/1/Auto",
"name": "/SwitchableOutput/1/Auto",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1010,
"y": 200,
"wires": []
},
{
"id": "46d2e8b13df2c229",
"type": "victron-output-custom",
"z": "a970ae66d296654b",
"service": "com.victronenergy.acload/58",
"path": "/SwitchableOutput/1/State",
"serviceObj": {
"service": "com.victronenergy.acload/58",
"name": "Pool-Heater (58)"
},
"pathObj": {
"path": "/SwitchableOutput/1/State",
"name": "/SwitchableOutput/1/State",
"type": "number",
"value": 1
},
"name": "",
"onlyChanges": false,
"roundValues": "no",
"rateLimit": 0,
"conditionalMode": false,
"condition1Operator": ">",
"condition2Enabled": false,
"condition2Service": "",
"condition2Path": "",
"condition2Operator": ">",
"logicOperator": "AND",
"outputTrue": "true",
"outputFalse": "false",
"outputOnChange": false,
"debounce": 2000,
"x": 1010,
"y": 260,
"wires": []
},
{
"id": "8b781c67a06ecdbf",
"type": "delay",
"z": "a970ae66d296654b",
"name": "delay",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 770,
"y": 260,
"wires": [
[
"46d2e8b13df2c229"
]
]
},
{
"id": "e83d1380812b63d4",
"type": "switch",
"z": "a970ae66d296654b",
"name": "Threshold 28°",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "27",
"vt": "num"
},
{
"t": "gte",
"v": "28",
"vt": "num"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 420,
"y": 240,
"wires": [
[
"2e4210e22c2a17c1"
],
[
"dbec5ef217b6f3b7"
]
]
},
{
"id": "2e4210e22c2a17c1",
"type": "change",
"z": "a970ae66d296654b",
"name": "Set On",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 600,
"y": 200,
"wires": [
[
"d730adf8e69094ed"
]
]
},
{
"id": "dbec5ef217b6f3b7",
"type": "change",
"z": "a970ae66d296654b",
"name": "Set Off",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 590,
"y": 260,
"wires": [
[
"8b781c67a06ecdbf",
"d730adf8e69094ed"
]
]
},
{
"id": "914dbe33d88143af",
"type": "global-config",
"env": [],
"modules": {
"@victronenergy/node-red-contrib-victron": "1.7.13"
}
}
]
The temperature values are - in my case - just a readout from my home-mqtt, Node-RED offers various ways to bring in some values into the virtual temperature sensors used here.


