You can download the complete notes for this project in a pdf form from here: SWHController_Article.pdf

Download the pdf button

The main project page is here: An Arduino Based Solar Water Heater Controller.

The project describing the same solar water heater controller system for the lovely flowery perfect world where designers and computer developers live is described here: Perfect World Solar Water Heater Controller.


In the real world there are imperfections. These require some modification of the perfect world solution described previously. This section describes the modifications I made to the solar water heater controller to suit the conditions and challenges encountered as I installed it into our solar water heater system. If you are looking at this project and attempting it on your own system you will find that some of the challenges mentioned here don’t apply.

The description of the “perfect world solution” (for this solar water heater controller that is) can be found here: Perfect World Solar Water Heater Controller.

What Challenges the Real World Has in Store

When the time comes to install a retrofit system like this there are all sorts of challenges that rear their heads when you are scrambling around on the roof trying to connect this to that while still ensuring it will work properly. Here are the main issues I grappled with with my system.

Arrangement of the System

The original system has a not got ideal locations for the temperature sensors, and so there is some lag between target temperatures on the panel being reached and the temperature reaching the sensors when the pump is off. This is because the heat has to travel through the metal pipework and stationary water to get to the sensor.

No temperature sensors are located in the hot water storage tank either. The temperature used is on a mixing junction where the hot water is drawn from the tank to be circulated through the panel and where fresh make up water is added. This has the advantage in being responsive to cold water being added to the system. Newly added cold water will be drawn into the solar water heater circulation system before going into the hot water storage tank.

Real World System Components
Click on the image for a high resolution version.

Where it would have been nice to have the stagnation temperature sensor (Tstag) attached to a bit of solar panel absorber within the panel enclosure, this was not possible. The best approximation was mounting it on the roof beside the panel where it would be exposed to the same amount of sunlight, wind, rain, and all other weather conditions affecting the solar panel.

Existing Equipment

Because I was retrofitting the controller to an existing system there were a number of key items that were reused (aside from the obvious ones like the panel and the hot water storage tank).

Pump and it’s Protection

The pump is a Wilo Star RS 25/6 circulation pump. These are pretty common and well made. It has run for many years and never missed a beat and it is likely to run happily for many years yet. When I was trying to solve the electrical noise issue one of my first thoughts was “Grrrr! Noisy cheapo pump.” This wasn’t the case at all, the Wilo pump was well protected with built in capacitors and compliant with various standards concerning electrical noise and interference. So, good pump, no problems there.

The pump is set for 99W output and so will draw no more than 0.41A.

If you are interested, here is the pump curve from the Wilo Star RS 25/6 datasheet.

Wilo Star RS-25/6 Pump Curves

Source: Wilo Datasheet: Star RS-25/6


As discussed above, I decided to make use of the existing Thermistors because they were designed to fit snuggly into the sensor pockets in the pipe fittings around the solar water heater system. I probably would have struggled to get as good thermal contact between the sensors and the pocket walls as the original sensors were getting.

The thermistors were unknown so I had to investigate how their resistance changed with temperature in order to find the Beta value for the thermistors and hence the expression for their temperature readings. I used a method as described here: Finding an Unknown Thermistor’s resistance curve.

Because the thermistors were already mounted in a place where I had no control over the temperature being measured, I took a number of readings over the course of a day from when the system was cold to when it was running full power on a bright sunny day. My method was to disconnect the sensor and read its resistance and then reconnect it to the existing controller (it was still able to read the sensors) and get it to display the temperature. When the system was running full power during the heat of the day, I would bypass the controller and connect the pump directly to the mains supply so the water would still circulate.

The end result was a series of readings spanning the typical range of temperatures the system would be likely to encounter.

I was able to calculate a Beta value of 3410K.

Now that the system is installed and running I have observed that the two existing thermistors appear to read consistently 1.5°C lower than the temperature readings from the other thermistors with the known Beta Value. I suspect the problem is not with the method used to determine the Beta Value but with the readings from the original controller. Given that the Senztek controller was not all that well set, I shouldn’t be surprised that it doesn’t calculate the temperatures from the thermistors correctly.

Logic for the Real World System

The logic shown here differs a little from the “perfect world” solar water heater controller system in that the Stagnation temperature sensor’s role has been diminished by using set points that mean that it has the pump enabled most of the time. Also, the inlet temperature sensor is only for reporting now rather than having a role governing the pump operation. This change was partly due to the mismatch between the inlet and outlet temperature sensors and the potential risk of this causing poor performance. Instead the temperature differential between the mixer at the base of the hot water storage cylinder and the solar water heater collector outlet was used to govern the pump’s operation.

Logic for controlling the Solar Water Heater Pump in the real world arrangement
Click on the image for a readable version of this image

Circuit for the Real World System

The circuit diagram and perf’ board arrangement for the system are shown below. This is unchanged from the circuit for the “Perfect World” solar water heater controller.

Solar Water Heater Controller Circuit Schematic
Circuit Diagram - Click on the image for Hi-res version.

Solar Water Heater Controller Perf Board Layout
Circuit Board Layout – perf’ board - Click on the image for Hi-res version.

At the heart of the system is an Arduino Nano. The demands on the Arduino are not large and so any Arduino could be used.

Opto Isolated Relay Module

An opto-isolated relay module from Banggood was selected to control the pump. Flyback diodes and various LEDs are included in the package. All it requires is a ground connection, 5v supply, and a signal. Using the jumper on the module the relay can be set to trigger with a high or a low signal. The manufacturer claims the relay can handle 240V 10A AC. I have seen a few comments in various forums that suggest the terminal connections themselves are not capable of carrying that sort of current, but the pump used in our solar water heater system only draws 0.5A anyway, so the module is quite suitable for the task.

The LCD screen is a typical 16x2 type. I have not connected the back light because that would be a little pointless.

The other components of significance are the thermistors and the 5V regulator.

Here is a list of the components used.

Coding for the Real World System

Here is the code for the real world system.

Hopefully the comments through the sketch will explain how it works.

You can download the sketch from here: SWHControllerv5.ino

The sketch does not use any libraries that don’t come with the standard Arduino IDE. I think it works on older versions of the Arduino IDE too. In short, there is nothing particularly fancy in this coding.

 * SWHControllerv5.ino
 * Hamish Trolove 2017 -
 * This sketch is to run a solar water heater and optimise the solar heat
 * yield.  It is also designed to be easily modified to suit local conditions.
 * A process similar to the Debounce routine in the Arduino Cookbook (pg155)
 * has been used to protect against short cycling of the pump. In addition to
 * this the Array average process ( 
 * has been used on all data to smooth the temperature readings and gain better
 * resolution.
 * This version reduces the effect of the Stagnation temperature for running
 * the pump while heating.  The reason for this is that the Stagnation Temperature 
 * enclosure as built cannot quite get up to the temperature of the panel which 
 * suggests it suffers from too much heat loss.  If the stagnation temperature
 * sensor can be installed in the Solar Water Heater enclosure then
 * SWHControllerv4.ino or SWHControllerv4a.ino would be able to be used.
 * In this version Tcyl and Tout are the previous controller's sensors which appear to be
 * 10kohm NTC thermistors with Beta values of 3410.
 * Tin has also been sidelined and in this version is only supplying information
 * for display on the screen.  It is not used in the logic during normal conditions.
 * There are four thermistors which are located;
 * Solar Water Heater Inlet - Tin
 * Solar Water Heater Outlet - Tout
 * Tank freshwater and SWH intake junction - Tcyl
 * and Tstag on a black plate in a pyrex tube to measure stagnation
 * temperatures and frost conditions.
 * The temperatures will control the operation of a pump through an optically isolated relay.
 * Each thermistor reading is displayed on the 16x2 LCD screen.  
 * Four buttons are connected.  At this stage only one is used and that is an override to turn
 * on the pump.
 * Circuit is described on
 * Connections are:
 * 10K B=4100 NTC Thermistors with 10K resistors in voltage dividers on A0, A1, A2, and A3
 * Push buttons with pull up resistors on D02, D03, D04, and D05
 * LED to indicate Frost Protection mode on D13
 * Pump relay control line on D06 (also connected to an LED)
 * LCD connections:
 * D12 to LCD RS pin
 * D11 to LCD E pin
 * D10 to LCD D7 pin
 * D09 to LCD D6 pin
 * D08 to LCD D5 pin
 * D07 to LCD D4 pin
 *The LCD panel is not lit.

#include <LiquidCrystal.h>
#include <math.h>  // We need this to calculate temperature values from thermistors. 
const int ThermPinA = A0;  // The Tin thermistor on Analogue Pin 0
const int ThermPinB = A1;  // The Tout thermistor on Analogue Pin 1
const int ThermPinC = A2;  // The Tcyl thermistor on Analogue Pin 2
const int ThermPinD = A3;  // The Tstag thermistor on Analogue Pin 3

const int ButtonA = 02; // Some button pins defined (Pump Override)
const int ButtonB = 03;
const int ButtonC = 04;
const int ButtonD = 05;

const int LEDFrost = 13; // Front protection mode indicator LED
const int LEDPump = 6; // Pump running indicator LED.

boolean PumpFlag = false; //This is the flag to control the pump.  All
                          //control logic acts on this flag.
boolean FrostRisk = false; //Frost risk flag for driving LED.
boolean PumpFlagOld = false; //Flag to hold previous PumpFlag State
boolean ConfirmPumpRun = false; //Once all debouncing has occurred this is
                                //the flag to run the pump.

int CycleCount = 0;  //A counter for the number of times the loop has run for
                     //while awaiting stable readings.
int ThermValue = 0; // Somewhere to stick the raw Analogue Pin Value.
double Tin = 0.00; // Calculated Temperature Values after smoothing
double Tout = 0.00;
double Tcyl = 0.00;
double Tstag = 0.00;

//Arrays to hold historic data
const int numReadings = 40;

double TinArray[numReadings];      // the readings from the Temperature sensors
double ToutArray[numReadings];
double TcylArray[numReadings];
double TstagArray[numReadings];

double Tintotal = 800;   // the running totals for the temperature readings
double Touttotal = 800;  //Starting value is 20 x 40 ie starting value
double Tcyltotal = 800;  //in arrays.  If the arrays had been loaded with
double Tstagtotal = 800; //zeros then these figures would also be zero.

int readIndex = 0;              // the index of the Historic Data Array Pointer

double FrostTemp = 4.00; //Temperature below which Frost is a risk.
                      //Adjust for local conditions.
double SWHFrostThresh = 5.00; //Temperature at SWH at which pump
                      //will start if running in Frost protection mode.
double SWHOutHeatedTemp = 8.00; //Temperature which will indicate that
                      //hot water is in SWH for frost protection.                      
double SWHCylTdiff = 2.50; //Temperature difference between cylinder
                      //and solar water heater before pumping starts.
double SWHTdiff = 1.50;  //Min temperature difference between cylinder
                      //and solar water heater before pumping is stopped.

LiquidCrystal lcd( 12, 11, 7, 8, 9, 10 );   //Pins for the LCD Screen

void setup()
  lcd.begin(16, 2);  //set up the LCD's number of columns and rows:
  lcd.setCursor(0, 0);
  lcd.print("Starting");  //Print a message to the LCD
  pinMode(ThermPinA, INPUT);
  pinMode(ThermPinB, INPUT);
  pinMode(ThermPinC, INPUT);
  pinMode(ThermPinD, INPUT);
  pinMode(ButtonA, INPUT);  //This is the pump override button.
  pinMode(ButtonB, INPUT);  //spare
  pinMode(ButtonC, INPUT);  //spare
  pinMode(ButtonD, INPUT);  //spare
  pinMode(LEDFrost, OUTPUT);
  pinMode(LEDPump, OUTPUT);

  //Load the arrays with normal(ish) values to begin with.
  for (int Indexer = 0; Indexer < numReadings; Indexer++)
    TinArray[Indexer] = 20.00;
    ToutArray[Indexer] = 20.00;
    TcylArray[Indexer] = 20.00;
    TstagArray[Indexer] = 20.00;

void loop()
  //Look to see if the pump override button pressed.
    ConfirmPumpRun = true;
  else  //if pump override button not pressed then all is running automatically.
    //The debounce process looks to see if the PumpFlag has remained stable for
    //eight seconds or to a count of about 40 ... more or less.

    for(CycleCount = 0; CycleCount < 40; CycleCount++)
      PumpFlagOld = PumpFlag;

      //The next few bits are the smoothing process.
      //Remove the reading being replaced from the running total.
      Tintotal =  Tintotal - TinArray[readIndex];    // the running totals for
      Touttotal = Touttotal - ToutArray[readIndex];   //the temperature readings
      Tcyltotal = Tcyltotal - TcylArray[readIndex];
      Tstagtotal = Tstagtotal - TstagArray[readIndex];
      //Read the Thermistor Data into the arrays
      ThermValue = analogRead(ThermPinA);  //What is the raw value from the Voltage Divider?
      TinArray[readIndex] = TempCalc(ThermValue);  //Just duck out to the calculation routine.      
      ThermValue = analogRead(ThermPinB);
      ToutArray[readIndex] = TempCalc2(ThermValue);  //Note using TempCalc2 function for Beta 3410
      ThermValue = analogRead(ThermPinC);
      TcylArray[readIndex] = TempCalc2(ThermValue);  //Note using TempCalc2 function for Beta 3410
      ThermValue = analogRead(ThermPinD);
      TstagArray[readIndex] = TempCalc(ThermValue);

      //By comparison with the other thermistors, Tin reads low.  The following lines
      //have been added to correct this value.  Comment out as desired.
      TinArray[readIndex] =  TinArray[readIndex] + 0.0092 * TinArray[readIndex] + 1.28;

      //Add the new readings to the total
      Tintotal =  Tintotal + TinArray[readIndex];    //This process has swapped an old
      Touttotal = Touttotal + ToutArray[readIndex];  //value for a new one and updated the
      Tcyltotal = Tcyltotal + TcylArray[readIndex];  //total.  It is efficient with regard
      Tstagtotal = Tstagtotal + TstagArray[readIndex]; //to minimised double handling.
      //move the pointer along a bit.
      readIndex = readIndex + 1;
      //check to see if we've exceeded the array positions
      if (readIndex >= numReadings)
        readIndex = 0;  //reset the pointer position to start of arrays.

      //Calculate the 8 second average for use in the control logic to follow.
      Tin = Tintotal / numReadings; // Calculated Mean Temperature Values
      Tout = Touttotal / numReadings;
      Tcyl = Tcyltotal / numReadings;
      Tstag = Tstagtotal / numReadings;
      //Check whether there is a frost risk.
      if(Tstag < FrostTemp)
        //Frost risk conditions are present.
        FrostRisk = true;
        //Has the solar water heater cooled to SWHFrostThresh temperature?
        //This is indicated by either Tin or Tout being below the threshhold Temp.
        if( Tout < SWHFrostThresh || Tin < SWHFrostThresh)
          PumpFlag = true;  //Both SWH sensors are cold, turn on the pump
          //The panel is still a little warm
          if(Tout > SWHOutHeatedTemp)
            //The SWH has now got a fresh shot of hot water in it so the 
            //pump can be turned off.
            PumpFlag = false;
      else //The conditions are not at risk of freezing
        FrostRisk = false;
        //Check to see if the stagnation temperature is greater than 15degrees
        //or greater than 60% of the cylinder intake temperature.
        //If it is then enable the the pump.  If it's not then it's
        //probably a pretty awful day.
        if(Tstag > 0.6* Tcyl || Tstag > 15.00)
          //Is the SWH outlet temperature greated than the Tank cylinder temp
          //plus the threshhold value SWHCylTdiff.
          if(Tout > Tcyl + SWHCylTdiff)
            //The SWH is hotter than cylinder by an appropriate margin so
            //start the pump.
            PumpFlag = true;
          if(Tout < Tcyl + SWHTdiff)
            //The temperature difference across the panel is too low.  The quality of solar
            //intensity is too low.  Turn off the pump.
            PumpFlag = false;

          //There's no point running pump on a dull day.
          PumpFlag = false;
      //Checking the stability of the Pump state for both normal and frost operation.
      if(PumpFlagOld != PumpFlag)
        CycleCount = 0;  //Unstable conditions therefore reset the counter.

      // Display the readings on the LCD Display
      lcd.setCursor(0, 0);
      lcd.setCursor(7, 0);
      lcd.setCursor(0, 1);
      lcd.setCursor(7, 1);

    ConfirmPumpRun = PumpFlag;  //At last a status is decided for normal operation.

  digitalWrite(LEDPump, ConfirmPumpRun); //This makes the pump run and lights pump LED.


double TempCalc(int ThermIn) //For Thermocouples Tin and Tstag with beta values 4100
  double TempVal = 1/((1/298.00)+(1/4100.00)*log(1024.00/ThermIn-1.00));
  TempVal = TempVal - 273.00;  //Note the extra decimal places force float usage.
  return TempVal;

double TempCalc2(int ThermIn2) //For Thermocouples Tout and Tcyl with beta values 3410
  double TempVal = 1/((1/298.00)+(1/3410.00)*log(1024.00/ThermIn2-1.00));
  TempVal = TempVal - 273.00;  //Note the extra decimal places force float usage.
  return TempVal;

What Could be Better.

So this project was a one off, and I have no intention of going through it again to make it all perfect. But having gone through the project here are a few thoughts about what could be done better if I were to do it again.

Isn’t hindsight is a wonderful thing?


The information provides is accurate to the best of my knowledge, but describes what I did and what worked for me. No responsibility is accepted for any failure of equipment or damage caused by attempting to follow these instructions. You’re probably working with main electricity, so watch what you’re doing with that stuff. It bits hard. The information is also provided in an as-is-where-is basis and as such I cannot provide support to help you solve your own problems you may encounter.