SALES INQUIRIES: 1 (888) 767-9864

Arduino Solar Power Control System :: Member Project

When Ray installed his new solar electric system, he was excited about the ability to implement this to use solar energy for his home.

However, he was in for a sunny surprise when he discovered that he either did not have enough solar energy to supply the loads he had connected or had an excess of solar energy that was going to waste.

But Ray didn’t let this stop him at all! He decided to come up with an Arduino power control system that allowed him to have more control over just how the rays of sunshine he collected went to powering his home. Take a look below at his radiant creation!

 

Arduino, solar, controller

Solar Power Controller showing the AUTO mode screen

Hi Ray! So, tell us about your project.

I designed a control system that will provide load shedding/load leveling.

The controller continually examines the amount of solar energy available and connects or disconnects loads such that the maximum amount of available solar power is being used, and thus minimizes the use of grid power, protects the battery bank from being too deeply discharged, and lengthens the battery life.

arduino, load shedding, load shedding design, solar controller

Whoa, wait a minute. What a cool idea! Why did you decide to build this?

In 2017, I purchased a 4400-watt two-phase solar electric system that could generate enough electric power to power my entire household whenever solar energy was available.

It consisted of 12 solar panels, 12 AGM batteries, an inverter, a charge controller for the batteries, and a system controller.

Unlike the more typical solar systems currently being installed, which simply generate power to be sold back to the utility company, this system was designed to provide household power even in the event of a grid power failure.

However, the delivery system did not give me sufficient control to avoid a lot of ongoing human interaction to operate the system. So, I elected to design my own controller as a ‘front end’ to the system.

That’s some great innovation there! So, tell us more about how your project works?

Well, the controller allows the optimal electrical load to run on solar power. When solar energy is not available (such as at night) all loads are normally connected to grid power and, if necessary, the batteries are recharged from grid power.

Five relays (dubbed R1 through R5) switch their respective two-phase loads between solar and grid power. Relays R1 and R2 are used to remove grid power from the load group for a fraction of a second when they are being toggled.

This prevents the possibility of creating an arc between solar and grid power.

The inverter is programmed in a mode called Auto Connect. When grid power is available at the inverter input, inversion ceases and the inverter is bypassed, powering any solar loads directly from the grid.

Thus, R4 controls the presence or absence of grid voltage at the inverter input. All eight relays are also controlled by the Controller.

There is also a timer running in the background which interrupts the loop code every minute. If any major changes in the system state have occurred since the last interrupt, appropriate action is taken.

Every 15 minutes, the solar conditions are re-evaluated, and an additional load group is placed on solar power if sufficient solar power is available, one load group is removed and placed on grid power if not, or the load is left unchanged.

Wow, that’s amazing. Can you elaborate a bit on all this information being sent to the Controller?

Inputs to the Controller consist of the following measurements: grid voltage, battery voltage, battery current, and solar energy.

The grid voltage is measured to sense grid power failures. Battery voltage I measured to sense the battery condition. Battery current is measured to sense if the battery is being charged or discharged.

Available solar energy is measured by a pyranometer.

Such a plethora of information being sent to the Controller! How exactly do you, well, control the Controller?

Operating the Controller is done via three pushbutton switches.  These pushbuttons create hardware interrupts.

Thus, the loop code consists of continually checking for interrupt flags and taking appropriate actions. The MODE button allows the user access to all of the available screens.

The INCrement button allows the user to choose between the various options shown on the current screen.  For example, in the MODE screen, the user chooses another screen that they desire to view.

In the MAN screen, the user chooses which relay he wishes to toggle. The SELect button allows the user to select the choice made via the INC button.

arduino, solar control, pushbutton, menu, map, mode screen, manual screen, auto screen

Wow! Could you also tell us more about how the users interact with your program?

Six different LCD screens are available to the user.  In the MAN screen, the eight relays are directly controlled.

This MANual mode is intended only for handling problem conditions that could occur. The AUTO screen displays the load groups currently being powered by solar.

The intent is that the controller will remain in this mode without human intervention for very long periods of time (months). It is designed to handle the presence or absence of grid power, low battery conditions, etc., without human intervention.

The other screens are the CONDition screen in which current system conditions are displayed, the HISTory screen showing the number of load groups that have been connected to solar power during the last 10 hours, the SHUTDOWN screen that provides a well-behaved shutdown procedure, and the MODE screen which allows the user to switch between different screens.

So, if there is a manual mode, can you talk about how the AUTO mode works as well?

The AUTO mode is where the controller is intended to operate essentially all the time, without interruption, without regard to variations in operating conditions. It is designed to operate in a variety of conditions.

The three basic system parameters are

  • The presence or absence of Grid Power.
  • The presence or absence of Solar Energy.
  • The battery voltage condition, whether above or below desired levels.

Every 15 minutes the auto mode routine is executed.

If solar is available, the controller decides whether to add or remove a load group from solar or to leave the load as is, based on whether the batteries are currently being charged or discharged. That way, the use of solar power is maximized without needlessly discharging the batteries.

Every minute, system conditions are examined to see if any of the three basic system parameters have changed.  If so, auto mode is executed and the 15-minute cycle clock is reset.

arduino, solar power controller, solar panel, inverter

The entire solar panel including the solar panel charge controller on the right, the inverter and associated equipment in the center, and the front-end Arduino controller on the left

Were there some aspects of this project that you struggled with?

Well, the simplicity of the operation of this device does come at a cost.

The software which implements all the various possibilities of pushbutton combinations is quite involved and required about 800 lines of Arduino/C++ code.

Congratulations on completing such a task! What other components did you use in your project?

The controller is implemented by an Arduino MEGA2560 microcomputer programmed in Arduino/ C++ language.

It consists of the MEGA2560, a 40 character by 4-line LCD display, three push buttons, an on/off switch, and all of the necessary signal conditioning and interface circuitry.

Arduino code:

Ray was kind enough to share his source code with us. You can see his code listed below:

//This sketch implements the software-based, LCD version of the solar controller.
//Written by M. Ray Johnson, Beginning August 2018 until April 2019.  This algorithm assumes the Inverter is
//placed in the AUTO CONNECT mode.

//UPDATES:
//18March2019: Eliminated all unnecessary relay closures and increased the cycle time to 15 minutes to increase
//relay life.

#include <TimerOne.h>
#include <LiquidCrystal.h>                                          //set up the LCD

const int RS=12, E1=11, E2=10, D4=7, D5=6, D6=5, D7=4;
LiquidCrystal lcd1(RS, E1, D4, D5, D6, D7);               //top two lines of display
LiquidCrystal lcd2(RS, E2, D4, D5, D6, D7);               //bottom two lines of display

volatile boolean incFlag = LOW;                                 //declare variables used in ISRs
volatile boolean selFlag = LOW;
volatile boolean modeFlag = LOW;
volatile boolean timerFlag = LOW;
volatile boolean currentSolarState = LOW;
volatile boolean currentGridState = LOW;
volatile boolean currentBatState = LOW;
volatile boolean changeFlag = LOW;
volatile boolean cycleFlag = LOW;                              //LOW indicates vB has dropped to vBminLow;
volatile boolean vBflag = HIGH;                                   //HIGH indicates vB has risen to vBminHigh.
volatile boolean oldSolarState = LOW;
volatile boolean oldGridState = LOW;
volatile boolean oldBatState = LOW;
volatile boolean incEnable = LOW;                             //LOW disables inc button interrupts
volatile boolean selEnable = LOW;
volatile boolean timerEnable = LOW;
volatile boolean relayState[] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};  //HIGH indicates de-energized.
                                                                                          //For the LG relays, HIGH indicates the load is
volatile byte manCursorPos = 0;                                 //connected to GRID power; LOW to SOLAR power.
volatile byte index = 0;                                                 //For G1 and G2, HIGH indicates a closed condition,
volatile byte mode = 0;                                                //connecting their respective LGs to GRID.  For
volatile byte horizCursorPos = 0;                               //the IV relay, HIGH indicates a closed condition,
volatile byte solarLoadCount = 0;                              //connecting Grid power to the inverter input.
volatile byte loadStateHistory[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
volatile byte controlState = 0;
volatile byte manIndex = 0;

volatile int powerSources = 0;
volatile int minutes = 0;
volatile int sixSec = 0;

int iBint = 0;
int vBint = 0;

volatile float iB = 0.0;
volatile float pyra = 0.0;
volatile float vB = 0.0;
volatile float vG = 0.0;
volatile int vDir = 0;
volatile int iDir = 1;
volatile unsigned int eventTime = 0;

const float pyraMin = 0.4;
const float vGmin = 100.0;
const float vBminHigh = 53.0;
const float vBminLow = 48.0;
                            
String man1 = "MANUAL MODE";                                          //LCD screen string arrays
String man2 = "Control load group relays as desired";
String man3 = "LG1  LG2  LG3  LG4  LG5  G1   G2   IV   ";
String man4 = "                           ";
String man6 = "All loads on Grid Power        ";
String man7 = "IV cannot open until all LGs are on GRID";
String man10 = "All loads are without power";                    //manSelect are relay state labels
String manSelect[] = {"GRID ","GRID ","GRID ","GRID ","GRID ", "CLO  ", "CLO  ", "CLO  "};
String mode1 = "MODE SELECT";
String mode2 = "Select one of the following modes";
String mode3 = "MAN    AUTO   COND   HIST   SHUTDWN";
String auto1 = "AUTO MODE";
String auto3 = "Grid and Solar Power are available      ";
String auto4 = "Grid Power failure; Solar is available  ";
String auto5 = "Grid Power is available; Solar is not   ";
String auto6 = "Grid Power failure; Solar not available ";
String auto7 = "Load Group(s) on Solar:  ";
String auto15 = "Battery Low";
String menu5 = "CURRENT CONDITIONS";
String shutdwn1 = "PLEASE WAIT";
String shutdwn2 = "PLEASE TURN THE CONTROLLER OFF NOW";
String history1 = "SOLAR LOAD HISTORY";

const byte relay1 = 22;                //Load Group 1 relay LOW is SOLAR (energized)
const byte relay2 = 23;                //Load Group 2 relay
const byte relay3 = 24;                //Load Group 3 relay
const byte relay4 = 25;                //Load Group 4 relay
const byte relay5 = 26;                //Load Group 5 relay
const byte relayG1 = 27;             //Grid relay for Load Groups 1,2.  LOW is open (energized)
const byte relayG2 = 28;             //Grid relay for Load Groups 3,4,5.  LOW is open (energized)
const byte relayIv = 29;               //Inverter Input relay.  LOW is open (energized)
const byte batCurrent = A10;     //Battery Current 0 to 1023 counts = -25 to +25 amps.  Was A4
const byte pyranometer = A7;   //0 to 5.0 VDC signal.  Was A0
const byte batVoltage = A8;       //0 to 66.7 VDC.  Was A2
const byte gridVoltage = A9;      //5.12 VDC corresponds to 125 VAC (RMS).  Was A3
const int arcDelayPeriod = 20;   //current arc delay period in ms
                                                         //Analog input A1 is apparently faulty.  Move all analog
                                                         //inputs to A7 thru A10

void setup() {
  Serial.begin(9600);                     //enable the computer serial monitor
  lcd1.begin(40,2);                        // set up the LCD's number of columns and rows:
  lcd2.begin(40,2);
  pinMode(4, OUTPUT);               //Define digital I/O, LCD data and control lines
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(12, OUTPUT);

  pinMode(relay1, OUTPUT);             //Relay control lines - Load Group 1 Relay
  pinMode(relay2, OUTPUT);             //LG2 Relay
  pinMode(relay3, OUTPUT);             //LG3 Relay
  pinMode(relay4, OUTPUT);             //LG4 Relay
  pinMode(relay5, OUTPUT);             //LG5 Relay
  pinMode(relayG1, OUTPUT);          //G1 Relay
  pinMode(relayG2, OUTPUT);          //G2 Relay
  pinMode(relayIv, OUTPUT);            //IV Relay
  
  pinMode(pyranometer, INPUT);    //Define analog inputs. 0 to 5.0 VDC signal
  pinMode(gridVoltage, INPUT);       //0 to 125 VAC (RMS)
  pinMode(batVoltage, INPUT);        //0 to 60 VDC
  pinMode(batCurrent, INPUT);         //-25 to +25 amps
  
  attachInterrupt(digitalPinToInterrupt(2), isrMode, RISING);          //set up 1st priority interrupt (Mode, on pin 2)
  attachInterrupt(digitalPinToInterrupt(3), isrIncrement, RISING);  //set up 2nd priority interrupt (Inc, on pin 3)
  attachInterrupt(digitalPinToInterrupt(18), isrSelect, RISING);       //set up 3rd priority interrupt (Select, on pin 18)
  Timer1.initialize(6000000);                                                                  //set up 4th priority interrupt (Timer, every 6 sec)
  Timer1.attachInterrupt(isrTimer);

  modeScreen();                                     //launch the mode screen
  digitalWrite(relayG1, LOW);             //open G1
  digitalWrite(relayG2, LOW);             //open G2
  delay(arcDelayPeriod);
  digitalWrite(relayIv, HIGH);              //close IV
  relayState[7] = HIGH;
  manSelect[7] = "CLO  ";
  for(byte i = 0; i < 5; i++) {                   //de-energize all Load Group relays  = HIGH; 
    manSelect[i] = "GRID ";}
  delay(arcDelayPeriod);
  digitalWrite(relayG1, HIGH);             //close G1
  relayState[5] = HIGH;
  manSelect[5] = "CLO  ";
  digitalWrite(relayG2, HIGH);            //close G2
  relayState[6] = HIGH;
  manSelect[6] = "CLO  ";
  modeFlag = LOW;                               //reset all the interrupt flags
  incFlag = LOW;
  selFlag = LOW;
  timerFlag = LOW;
  cycleFlag = LOW;
  solarLoadCount = 0;
  index = 0;
  manIndex = 0;
  horizCursorPos = 0;
  manCursorPos = 0;
  checkStateChange();                           //initialize the change state conditions
  oldSolarState = currentSolarState;
  oldGridState = currentGridState;
  oldBatState = currentBatState;
  changeFlag = LOW;
  vBflag = HIGH;
  minutes = 0;
  for(int i = 0; i < 40; i++) {                 //initialize history
    loadStateHistory[i] = 0;}
  controlState = 0; }                           //end of setup

void loop() {
  if(modeFlag == HIGH) {                  //check to see if Mode Button has been pressed 
    delay(300);                                     //debounce
    modeFlag = LOW;
    if(mode == 0) {                               //if I'm in mode screen
      controlState = 0;
      modeScreen();      
      return;}
    else if(mode == 1) {                       //if I'm in man screen
      manCursorPos = horizCursorPos;           //save the cursor position
      manIndex = index;
      modeScreen();
      return; }
    else if(mode == 2) {                       //if I'm in auto screen
      modeScreen();
      return; }
    else if(controlState == 0) {           //if I'm in no control
      modeScreen();
      return; }
    else if(controlState == 1) {           //if I'm in man control
      horizCursorPos = manCursorPos;           //restore the cursor position
      index = manIndex;
      manMode();
      return; }
    else {                                               //I'm in auto control
      autoMode();
      return; } }
  else if(incFlag == HIGH) {               //check to see if Increment Button has been pressed
    delay(300);                                    //debounce
    incFlag = LOW;
    if(mode == 0) {                              //if I'm currently in Mode screen                  
      if(horizCursorPos < 28) {           //move the cursor in the Mode screen
        horizCursorPos += 7;}
      else horizCursorPos = 0;
      lcd2.setCursor(horizCursorPos,0);        //reposition the cursor
      index = horizCursorPos/7;                      //point to the current menu item
      return; }
    else {                                              //I'm currently in Man screen
      if(horizCursorPos < 35) {          //move the cursor in the Man screen
        horizCursorPos += 5;}
      else horizCursorPos = 0;
      lcd2.setCursor(horizCursorPos,1);        //reposition the cursor
      index = horizCursorPos/5;                      //point to current relay
      return; } }    
  else if(selFlag == HIGH) {              //check to see if the Select Button has been pressed
    delay(300);                                   //debounce
    selFlag = LOW;
    if(mode == 0) {                             //if I'm in the mode screen
      if(index == 0) {                           //if Man Mode has been selected
        horizCursorPos = manCursorPos;         //restore the cursor position
        index = manIndex;        
        manMode();                             //launch manMode
        return; }
      else if(index == 1) {                    //if Auto Mode has been selected
        minutes = 0;                             //start the cycle clock
        if(solarLoadCount > 0) {         //adjust IV according to solarLoadCount
          if(relayState[7] == HIGH) {   //if IV is closed, open it
            relayState[7] = LOW;
            manSelect[7] = "OPEN ";
            digitalWrite(relayIv, LOW); } }
        else {
          if(relayState[7] == LOW) {    //if IV is open, close it
            relayState[7] = HIGH;
            manSelect[7] = "CLO  ";
            digitalWrite(relayIv, HIGH); } }        
        autoMode();                             //launch autoMode       
        return; }
      else if(index == 2) {                    //if Conditions Mode has been selected
        conditionsScreen();                 //launch conditionsScreen
        return; } 
      else if(index == 3) {                    //History Mode has been selected
        historyScreen();                        //launch history screen
        return; }
      else {                                            //Shutdown has been selected
        shutdwnScreen();                    //launch shutdown screen
        return;} }
    else {                                             //I'm currently in the Man screen
      manToggle(); 
      manCursorPos = horizCursorPos;           //save the cursor position
      manIndex = index;
      return;} }
  else if(timerFlag == HIGH) {        //check timer flag
    timerFlag = LOW;
    if(sixSec >= 10){
      sixSec = 0;
      checkVbFlag();
      checkStateChange();
      if(mode == 4) conditionsScreen();
      if(mode == 5) historyScreen();
      if(changeFlag == HIGH) {         //if the system state has changed
        Serial.print("State change detected.  Time = ");
        eventTime = millis()/60000;
        Serial.println(eventTime);
        Serial.print("vB = ");
        Serial.println(vB);
        Serial.print("pyra = ");
        Serial.println(pyra);
        Serial.print("vG = ");
        Serial.println(vG);      
        if(mode == 2) goto t1;
        else if(mode == 1) {
          manMode();
          return; }
        else return; }
      else if(controlState != 2) return;
      else {
        printCycle();                            //update the cycle display
        if(minutes >= 15) {                 //if 15 minutes have passed
t1:       cycleFlag = HIGH;
          minutes = 0;
          updateHistory();
          autoMode();
          return; }      
        else {
          minutes ++;
          return;} } }
    else {
      sixSec ++;
      return;} }
  else return; }                               //end of loop          

                                      //*****PUT CALLED FUNCTIONS HERE*****

void modeScreen(void) {               //This function clears the display, then creates the Mode Screen
  lcd1.clear();                            
  lcd2.clear();
  lcd1.setCursor(0,0);
  lcd1.print(mode1);                       //print "MODE SELECT" on LCD line 1
  lcd1.setCursor(0,1);
  lcd1.print(mode2);                       //print LCD line 2
  lcd2.setCursor(0,0);
  lcd2.print(mode3);                       //print LCD line 3
  lcd2.setCursor(0,0);                     //turn on the blinking cursor
  lcd2.cursor();
  lcd2.blink();
  horizCursorPos = 0;
  index = 0;
  mode = 0;
  selEnable = HIGH;                        //enable select interrupts
  incEnable = HIGH;                        //enable increment interrupts
  timerEnable = LOW;                    //disable timer interrupts
  return; }                                         //end of modeScreen

void conditionsScreen(void) {     //This function clears the dsplay, then creates the
  lcd1.clear();                                  //Conditions Screen.
  lcd2.clear();
  lcd1.noCursor();
  lcd1.noBlink();
  lcd2.noCursor();
  lcd2.noBlink();
  lcd1.setCursor(0,0);
  lcd1.print(menu5);                       //"CURRENT CONDITIONS"
  printConditions();                         //print conditions on LCD lines 2 and 3
  readPowerSources();
  lcd2.setCursor(0,1);                     //print power sources on line 4
  if(powerSources == 3) lcd2.print(auto3); //"Grid and Solar Power are available"
  if(powerSources == 2) lcd2.print(auto4); //"Grid Power failure; Solar is available"
  if(powerSources == 1) lcd2.print(auto5); //"Grid Power is available; Solar is not"
  if(powerSources == 0) lcd2.print(auto6); //"Grid Power failure; Solar not available"
  selEnable = LOW;                         //disable Select Button interrupts
  incEnable = LOW;                         //disable Increment Button interrupts
  timerEnable = HIGH;                    //enable timer interrupts
  mode = 4;
  return; }                                         //end of Conditions Screen
  
void historyScreen(void) {            //This function clears the display, then creates the
  lcd1.clear();                                   //History Screen.
  lcd2.clear();
  lcd1.setCursor(0,0);
  lcd1.print(history1);                    //"SOLAR LOAD HISTORY"  on line 1
  lcd1.noCursor();
  lcd1.noBlink();
  lcd2.noCursor();
  lcd2.noBlink();
  printHistory();                              //print the load history on LCD line 4
  readPowerSources();
  lcd2.setCursor(0,1);                    //print power sources on line 4
  if(powerSources == 3) lcd2.print(auto3); //"Grid and Solar Power are available"
  if(powerSources == 2) lcd2.print(auto4); //"Grid Power failure; Solar is available"
  if(powerSources == 1) lcd2.print(auto5); //"Grid Power is available; Solar is not"
  if(powerSources == 0) lcd2.print(auto6); //"Grid Power failure; Solar not available"
  selEnable = LOW;                         //disable Select Button interrupts
  incEnable = LOW;                         //disable Increment Button interrupts
  timerEnable = HIGH;                    //enable timer interrupts 
  mode = 5;
  return; }                                         //end of historyScreen

void shutdwnScreen(void) {         //This function provides for a well-behaved
  lcd1.clear();                                   //shutdown process that creates the Shutdown                          
  lcd2.clear();                                   //screen, de-energizes all relays, invites the                         
  lcd2.noCursor();                           //user to turn off the controller, and enters
  lcd2.noBlink();                              //an endless loop.
  boolean b = LOW;                        //avoid unnecessary relay actions
  for(int a = 2; a < 5; a++) {
    if(relayState[a] == LOW) b = HIGH; }  
      if(b == HIGH) {
        digitalWrite(relayG1, LOW);      //Open G1
        delay(arcDelayPeriod);
        for(int q = 0; q < 5; q++) {
          if(relayState[q] == LOW) {
            solarLoadCount--;
            digitalWrite(q + 22, HIGH);
            relayState[q] = HIGH;
            manSelect[q] = "GRID "; } }
        delay(arcDelayPeriod);
        digitalWrite(relayG1, HIGH); }    //Close G1         
  digitalWrite(relayIv, HIGH);              //close IV
  selEnable = LOW;                               //disable select interrupts
  incEnable = LOW;                               //disable increment interrupts
  timerEnable = LOW;                          //disable timer interrupts
s1: lcd1.setCursor(0,0);
  lcd1.print(shutdwn2);                       //LCD line 1: "PLEASE TURN THE CONTROLLER OFF NOW"
  delay(1000);
  lcd1.clear();
  delay(300);
  goto s1;                                               //endless loop
  return; }                                              //end of Shutdown Screen
  
void manMode(void) {                       //This function implements the Manual Mode.  It
  mode = 1;                                           //creates the appropriate screen depending on
  controlState = 1;                               //conditions.
  incEnable = HIGH;                             //enable Increment Interrupts
  selEnable = HIGH;                             //enable Select Interrupts
  timerEnable = HIGH;                        //enable Timer Interrupts
  readPowerSources();
  lcd1.clear();
  lcd2.clear();
  lcd1.setCursor(0,0);
  lcd1.print(man1);                             //"MANUAL MODE" on line 1 
  lcd1.setCursor(0,1);                         //print power sources on line 2
  if(powerSources == 3) lcd1.print(auto3);   //"Grid and Solar Power are available"
  if(powerSources == 2) lcd1.print(auto4);   //"Grid Power failure; Solar is available"
  if(powerSources == 1) lcd1.print(auto5);   //"Grid Power is available; Solar is not"
  if(powerSources == 0) lcd1.print(auto6);   //"Grid Power failure; Solar not available"
  lcd2.setCursor(0,0);
  lcd2.print(man3);                             //"LG1  LG2  LG3  LG4  LG5  G1   G2   IV   "
  lcd2.setCursor(0,1);
  for(int p = 0; p < 8; p++) {               //print current relay states on line 4
    lcd2.print(manSelect[p]); }
  lcd2.setCursor(horizCursorPos,1);          //put the cursor back where it was
  lcd2.cursor();
  lcd2.blink();
  checkVbFlag(); 
  if(vBflag == HIGH) {
    lcd1.setCursor(28,0);                    //clear the "battery Low" message
    lcd1.print("           "); } 
  else {
    lcd1.setCursor(28,0);
    lcd1.print(auto15); }                    //Print "Battery Low" message
  return; }                                           //end of manMode

void autoMode(void) {                   //This function implements the Auto Mode
  Serial.print("AUTO Mode executed.  Time = ");
  eventTime = millis()/60000;
  Serial.println(eventTime);
  incEnable = LOW;                           //disable increment interrupts
  selEnable = LOW;                           //disable select interrupts
  timerEnable = HIGH;                      //enable timer interrupts, for 5 minute load checks.
  checkVbFlag();                                //This function executes every five minutes or
  mode = 2;                                        //sooner if gross system conditions change.
  controlState = 2;
  if(manSelect[5] = "OPEN ") {        //if G1 is open, close it.  This is in case some 
    relayState[5] = HIGH;                  //idiot left G1 or G2 open when leaving MAN mode.
    manSelect[5] = "CLO  ";
    digitalWrite(relayG1, HIGH); }
   if(manSelect[6] = "OPEN ") {       //if G2 is open, close it.
    relayState[6] = HIGH;
    manSelect[6] = "CLO  ";
    digitalWrite(relayG2, HIGH); }   
  lcd1.clear();                                    //clear the auto mode Screen
  lcd2.clear();
  lcd1.setCursor(0,0);
  lcd1.print(auto1);                         //print "AUTO MODE" on LCD line 1
  horizCursorPos = 0;
  lcd2.noCursor();                           //turn off the blinking cursor
  readPowerSources();
  lcd2.noBlink();
  lcd1.setCursor(0,1);                    //print power sources on line 2
  if(powerSources == 3) lcd1.print(auto3);   //"Grid and Solar Power are available"
  if(powerSources == 2) lcd1.print(auto4);   //"Grid Power failure; Solar is available"
  if(powerSources == 1) lcd1.print(auto5);   //"Grid Power is available; Solar is not"
  if(powerSources == 0) lcd1.print(auto6);   //"Grid Power failure; Solar not available"
  if(vBflag == HIGH) {
    lcd2.setCursor(0,1);
    lcd2.print(man4);                      //clear the "Battery Low" message
    if(powerSources >= 1) {           //if either Grid or Solar power is available
      if(cycleFlag == HIGH) {            //if the cycle time is up
        cycleFlag = LOW;
        if(powerSources >= 2) {        //if solar is available
          adjustLoad();
          if(solarLoadCount == 0) {
a3:         lcd2.setCursor(0,0); 
            lcd2.print(man6);               //print "All loads on GRID ..." on LCD line 3
a4:         if(relayState[7] == LOW) {       //close IV
              relayState[7] = HIGH;
              manSelect[7] = "CLO  ";
              digitalWrite(relayIv, HIGH); }
a5:         printCycle();                     //print cycle time
            printIv();                              //print IV relay state
            return; }
          else {
a6:         if(relayState[7] == HIGH) {      //open IV
              relayState[7] = LOW;
              manSelect[7] = "OPEN ";
              digitalWrite(relayIv, LOW); }
a2:           lcd2.setCursor(0,0);
              lcd2.print(auto7);             //print LCD line 3
              solarLoadCount = 0;         //print the load groups on solar
              for (int q = 0; q < 5; q++) {
                if(relayState[q] == LOW) {
                  solarLoadCount++;
                  lcd2.print(q + 1);
                  lcd2.print("  "); } }
              goto a5; } }
        else {                                        //solar power is not available; put all load groups on Grid
a1:       boolean b = LOW;             //avoid unnecessary relay actions
          for(int a = 2; a < 5; a++) {
            if(relayState[a] == LOW) b = HIGH; }  
          if(b == HIGH) {
            digitalWrite(relayG1, LOW);      //Open G1
            delay(arcDelayPeriod);
            for(int q = 0; q < 5; q++) {
              if(relayState[q] == LOW) {
                digitalWrite(q + 22, HIGH);
                relayState[q] = HIGH;
                manSelect[q] = "GRID "; } }
            delay(arcDelayPeriod);
            digitalWrite(relayG1, HIGH); }    //Close G1
          solarLoadCount = 0;                   
          if(powerSources == 1 || powerSources == 3) goto a3;     //if Grid power is available
          else {
            lcd2.setCursor(0,0);            //print LCD line 3,  "All loads are without power" message
            lcd2.print(man10);  
            goto a4; } } }
      else goto a2; }
    else {                                            //put LG 1,2 on Solar; put LG 2,3,4 on Grid
      if(relayState[0] == HIGH || relayState[1] == HIGH) {  //avoid unnecessary relay actions
        digitalWrite(relayG1, LOW);          //Open G1
        delay(arcDelayPeriod);
        for(int q = 0; q < 2; q++) {     //connect LG 1 and 2 to Solar
          if(relayState[q] == HIGH) {
            digitalWrite(q + 22, LOW);
            relayState[q] = LOW;
            manSelect[q] = "SOL  "; } }
      solarLoadCount = 2;      
      boolean b = LOW;                     //avoid unnecessary relay actions
      for(int a = 2; a < 5; a++) {
      if(relayState[a] == LOW) b = HIGH; }   
      if(b == HIGH) {
        for(int q = 2; q < 5; q++) {       //connect LG 3, 4, and 5 to Grid
          if(relayState[q] == LOW) {
            digitalWrite(q + 22, HIGH);
            relayState[q] = HIGH;
            manSelect[q] = "GRID "; } } }
      delay(arcDelayPeriod);
      digitalWrite(relayG1, HIGH); }  //Close G1
      goto a6; } }
  else {
    lcd2.setCursor(0,1);
    lcd2.print(auto15);                      //Print "Battery Low" on LCD line 4
    goto a1; } }                                    //End of autoMode
 
//This function changes the state of the relay in the MAN display that has been selected for toggle.
//It switches the actual selected relay, and also syncs manSelect[] and RelayState[].  The global
//variable "index" points to the selected relay.   If any of the LG relays are toggled, G1 or G2 will
//be opened momentarily to provide arc protection.  Called only from manMode.
void manToggle(void) {
  if(index < 2) {                                  //if the selected relay is LG1 or LG2
    if(manSelect[5] == "CLO  ") {      //if G1 is closed, open it
      digitalWrite(relayG1, LOW);
      delay(arcDelayPeriod);}
    relayToggle();                                //Toggle the relay
    delay(arcDelayPeriod);
    digitalWrite(relayG1, HIGH);      //close G1
    goto m1; }
  else if(index < 5) {                           //if the selected relay is LG3, LG4, or LG5
    if(manSelect[6] == "CLO  ") {      //if G2 is closed, open it
      digitalWrite(relayG2, LOW);
      delay(arcDelayPeriod);}     
    relayToggle();                               //Toggle the relay
    delay(arcDelayPeriod);    
    digitalWrite(relayG2, HIGH);     //close G2
    goto m1; }
  else ctrlToggle();                            //if the selected relay is G1, G2, or IV, toggle it
m1: lcd2.setCursor(0,0);  
  lcd2.print(man3);                          //reprint LCD line 3
  lcd2.setCursor(0,1);    
  for(int p = 0; p < 8; p++) {             //print current relay states on line 4
    lcd2.print(manSelect[p]); }
  lcd2.setCursor(horizCursorPos,1);           //put the cursor back where it was
  return; }                                          //end of manToggle

void relayToggle(void) {                          //This function toggles the state of the load relays, LG1
  if(manSelect == "GRID ") {        //through LG5, pointed to by the global pointer 'index'.
    manSelect = "SOL  ";               //manSelect, relayState and the actual relay are kept in sync.
    relayState = LOW;                   //It is called only from manToggle.  This function DOES NOT
    digitalWrite((22 + index), LOW);       //provide arc protection which must be done in the calling
    solarLoadCount++; }                           //function.  solarLoadCount is also adjusted as needed.
  else {              
    manSelect = "GRID "; 
    relayState = HIGH;
    digitalWrite((22 + index), HIGH);
    solarLoadCount--; }
  return; }                                                  //end of relayToggle
      
void ctrlToggle(void) {                           //This function toggles the state of the control relay G1, 
  if(index > 6) {                                        //G2 or IV, pointed to by the global pointer 'index'.
    if(manSelect[7] == "OPEN ") {         //manSelect, relayState and the actual relay are kept in
      manSelect[7] = "CLO  ";                  //sync.  It is called only from manToggle.
      relayState[7] = HIGH;
      digitalWrite(relayIv, HIGH); }
    else {
      manSelect[7] = "OPEN ";
      relayState[7] = LOW;
      digitalWrite(relayIv, LOW); }
    return; }
  else if(manSelect == "CLO  ") {           //Toggle either G1 or G2 pointed to by 'index'
    manSelect = "OPEN ";
    relayState = LOW;
    digitalWrite((22 + index), LOW); }
  else {
    manSelect = "CLO  "; 
    relayState = HIGH;
    digitalWrite((22 + index), HIGH); }
  return; }                                              //end of ctrlToggle

 void readPowerSources() {                                 //This function identifies the current power
  measureVg();                                                       //sources (Grid and Solar) eturns the global
  measureSolar();                                                  //variable powerSources as follows:
  if(vG > vGmin && pyra >= pyraMin) powerSources = 3;      //0 = no power, 1 = grid power only,
  else if(vG >= vGmin && pyra < pyraMin) powerSources = 1; //2 = solar power only, 3 = both grid and solar.
  else if(vG < vGmin && pyra >= pyraMin) powerSources = 2;
  else powerSources = 0;                      
  return; }                                                               //end of readPowerSource

void adjustLoad(void) {                        //This function adjusts the number of load groups
  measureIb();                                        //connected to solar power.  It is called only from
  if(iB > 0.0) {                                          //function autoMode.  Global variable solarLoadCount
    if(solarLoadCount >= 5) return;     //contains the number of load groups currently
    else {                                                   //connected to solar power.  Basis for the load
      increaseLoad();                               //adjustment is whether or not battery current is
      return; } }                                         //positive.  If so, an attempt is made to add an
  else if(solarLoadCount == 0) return;         //additional Load Group to solar power.  If not,
  else {                                                    //a Load Group is removed.
    decreaseLoad();
    return; } }                                          //end of adjustLoad

void increaseLoad(void) {                 //This function attempts to add an additional load group to
  int i = -1;                                            //Solar power.  Loads are always added from relay1 to relay5.
c1:  i++;                                               //If solarLoadCount becomes greater than zero, IV is opened.
  if(i > 4) return;                                 //if all load groups have been tried
  if(relayState[i] == LOW) goto c1   //if this LG relay is already connected to solar
  if(i < 2) {
    digitalWrite(relayG1, LOW);        //Open G1
    delay(arcDelayPeriod);
    digitalWrite(i + 22, LOW);            //add one load group to solar, relay1 or relay2
    delay(arcDelayPeriod);
    digitalWrite(relayG1, HIGH); }    //Close G1
  else {
    digitalWrite(relayG2, LOW);        //Open G2
    delay(arcDelayPeriod);
    digitalWrite(i + 22, LOW);            //add one load group to solar, relay3, relay4, or relay5
    delay(arcDelayPeriod);
    digitalWrite(relayG2, HIGH); }     //Close G2
    manSelect[i] = "SOL  ";
    relayState[i] = LOW;
    solarLoadCount ++;
    if(relayState[7] == HIGH) {            //if IV is closed, open it
    relayState[7] = LOW;
    manSelect[7] = "OPEN ";
    digitalWrite(relayIv, LOW); }
    return; }                                           //end of increaseLoad

void decreaseLoad(void) {                //This function disconnects one load group from solar.
  int i = 5;                                              //Loads are always deleted from relay5 to relay1.  If
b1: i--;                                                   //solarLoadCount goes to zero, IV is closed.
  if(i < 0) return; 
  if(relayState[i] == HIGH) goto b1;
  if(i < 2) {
    digitalWrite(relayG1, LOW);         //Open G1
    delay(arcDelayPeriod);
    digitalWrite(i + 22, HIGH);             //remove one load group from solar power, relay1 or relay2
    delay(arcDelayPeriod);
    digitalWrite(relayG1, HIGH); }       //Close G1
  else {
    digitalWrite(relayG2, LOW);          //Open G2
    delay(arcDelayPeriod);
    digitalWrite(i + 22, HIGH);              //remove one load group from solar power, relay3, relay4, or relay5
    delay(arcDelayPeriod);
    digitalWrite(relayG2, HIGH); }       //Close G2
  delay(arcDelayPeriod);  
  manSelect[i] = "GRID ";                     //keep manSelect up to date       
  relayState[i] = HIGH;                          //keep relayState up to date
  solarLoadCount --; 
  if(solarLoadCount == 0) {                   //if all loads have been connected to Grid power
    if(relayState[7] == LOW) {               //if IV is open, close it
      relayState[7] = HIGH;
      manSelect[7] = "CLO  ";
      digitalWrite(relayIv, HIGH); }
    return; }
  else return; }                                       //end of decreaseLoad

void updateHistory(void) {                        //This function updates the load history array 
  for(byte m = 39; m > 0 ; m--) {                //adding the latest value on the left and shifting
    loadStateHistory[m] = loadStateHistory; } //previous values to the right, retaining only the
  loadStateHistory[0] = solarLoadCount;                  //latest 40 values (about 3 hours worth).
}                                                                     //end of updateHistory

void printHistory(void) {                           //this function updates solar connection history on
  lcd1.setCursor(0,1);                                 //LCD line 2
  for(byte z  = 0; z < 40; z++) {
    lcd1.print(loadStateHistory[z]); }
  return; }                                                     //end of printHistory

void printCycle(void) {                             //This function prints the cycle time (min:sec) at
  lcd1.setCursor(29,0);                             //the end of LCD line 1.
  lcd1.print("cycle ");
  lcd1.print(minutes);
  return; }                                                   //end of printCycle

void printIv(void) {                                  //This function prints the state of the IV relay on
  lcd2.setCursor(27,1);                            //the end of LCD line 4.
  lcd2.print("            ");
  lcd2.setCursor(27,1);
  lcd2.print("IVR = ");
  if(relayState[7] == HIGH) lcd2.print("CLOSED");
  else lcd2.print("OPEN");
  return;}                                                 //end of printIv

void measureVg(void) {                       //This function measures Grid Voltage
  int vGint = 0;                                        //Take 5 readings and average them
  for(int w = 0; w < 5; w++) {
    vGint = analogRead(gridVoltage) + vGint;
    delay(1);}
  vG = float(vGint/31.10);                    //Gives value in VAC
 return; }
 
void measureSolar(void) {                   //This function measures solar power availability
  int pyraInt = 0;                                     //Take 5 readings and average them
  for(int x = 0; x < 5; x++) {
    pyraInt = analogRead(pyranometer) + pyraInt;
    delay(1);}
  pyra = float(pyraInt)/1023.0;
  //pyra = 1.0;
  return; }

void measureVb(void) {                       //This function measures battery voltage
  vBint = 0;                                              //Take 5 readings and average them
  for(int y = 0; y < 5; y++) {
    vBint = analogRead(batVoltage) + vBint; 
    delay(1);}
  vB = float(vBint)/85.2;
  /*if(vDir == 1) {                                   //for test purposes
    vB = vB + 0.3;
    if(vB > 54.0) vDir = 0; }
  else {
    vB = vB - 0.3;
    if(vB < 46.0) vDir = 1; }*/
  return; }

void measureIb(void) {                       //Range is roughly -25 amp to +25 amps over the range
  iBint = 0;                                              //Take 5 readings and average them.  This is a very
  for(int z = 0; z < 5; z++) {                   //noisy meassurement.
    iBint = analogRead(batCurrent) + iBint;
    delay(1); }
  iBint = iBint/5;                                    //0 to 1023.  So zero current is about 512.
  iBint = iBint - 512;                              //get to zero offset
  iB = float(iBint) * 0.048828;             //convert counts to amps
  iB = (0.741 * iB) - 3.244;                   //conform to test data.  The offset was
  //iB = 1.0;                                            //originally -4.444.  I'm varying it to find
  /*if(iBflag == 1) { 
    iB = iB + 0.2;
    if(iB >= 1.0) iBflag = 0; }
  else {
    iB = iB - 0.2;
    if(iB <= -1.0) iBflag = 1; }
  Serial.print("iB = ");
  Serial.println (iB);*/
  return; }                                              //the best balance.
           
void checkVbFlag(void) {                   //This function changes vBflag depending on the
  measureVb();                                    //history of vB.
  if(vB >= vBminHigh) vBflag = HIGH;
  else if(vB < vBminLow) vBflag = LOW;
  else return; }
  
void checkStateChange(void) {        //this function checks the gross system state
  measureSolar();                                //(presence of solar, grid voltage, and high battery
  checkVbFlag();                                  //voltage) and compares them with values last measured.
  measureVg();                                    //If a major change has occured, changeFlag is set HIGH
  if(pyra > pyraMin) currentSolarState = HIGH; //and the current LCD screen is updated to reflect these
  else currentSolarState = LOW;                           //depending on the current mode.  It is called only from
  if(vG > vGmin) currentGridState = HIGH;         //the code that services the timer ISR, and is typically
  else currentGridState = LOW;                            //executed every minute if the timer interrupt is enabled.
  if(vBflag == HIGH) currentBatState = HIGH;
  else currentBatState = LOW;
  changeFlag = LOW;
  if(currentSolarState != oldSolarState) {
    changeFlag = HIGH;
    oldSolarState = currentSolarState; }
  if(currentGridState != oldGridState) {
    changeFlag = HIGH;
    oldGridState = currentGridState; }
  if(currentBatState != oldBatState) {  
    changeFlag = HIGH;
    oldBatState = currentBatState; }
  return; }                                           //end of checkStateChange

void printConditions(void) {           //this function measures current conditions and
  measureVg();                                  //prints them on LCD lines 2 and 3
  measureSolar();
  measureVb();
  measureIb();
  lcd1.setCursor(0,1);
  lcd1.print("Vg = ");
  lcd1.print(vG,1);
  lcd1.print("    Vb = ");
  lcd1.print(vB,1);
  lcd1.print("    IVR = ");
  if(relayState[7] == HIGH) lcd1.print("CLOSED");
  else lcd1.print("OPEN  ");
  lcd2.setCursor(0,0);
  lcd2.print("Pyra = ");
  lcd2.print(pyra,2);
  lcd2.print("    Ib = ");
  lcd2.print(iB,1);
  return; }                                        //end of printConditions

                                           //*****PUT ISRs HERE*****

void isrMode(void) {                 //first priority ISR - Mode Button, never disabled
  modeFlag = HIGH; }

void isrIncrement(void) {         //second priority ISR - Increment Button
  incFlag = incEnable; }              //raise the interrupt flag if it is enabled

void isrSelect(void) {                //third priority ISR - Select Button
  selFlag = selEnable; }              //raise the interrupt flag if it is enabled
  
void isrTimer(void) {                 //fourth priority ISR - Timer
  timerFlag = timerEnable; }     //raise the timer flag if it is enabled

Ray also included his design files for his project; check those out here:

Solar Power Controller Design File

Solar Power Controller Design File – Addeendum

About Ray:

Ray has a Ph.D. in Electrical Engineering, with experience programming in several different languages.

However, this is his first time programming in C++. He is an 80-year-old retiree who developed a newfound interest in Arduino as an affordable platform to accomplish his goals.

3 Comments

  1. Avatar Mike Koens on May 10, 2019 at 4:45 am

    Wow Ray, that is impressive. I have been considering a similar unit for my solar setup, which is very similar to yours,1.8kw of PV panels, 48 volt 600Ahr battery and a 5kw inverter with built in MPPT charger. The one thing I want to do differently, is to switch an optional extra load into the circuit, if the batteries reach full charge with plenty of solar input still available, such as a small aircon unit on the battery box, or an ice maker. I also want to run two inverters, a smaller 500 watt or 1 kw unit when load conditions are light. I find the 5 kw inverter is drawing nearly 2 amps from the battery all night, even without a load at all, and this is a waste of solar/battery power,whereas a smaller inverter will draw proportionally less power at low loads.

  2. Avatar Jay on May 14, 2019 at 12:13 am

    This is awesome, I live in Australia and we get rebates from the government to put up solar on our roof. Any chance you will consider releasing the lot of it as open source code and hardware? I don’t remember seeing where you lived but if you had sun like us this system would be so much better than the 1/4 of the cost you get back from putting it in the grid. So we may pay $0.28 per kw and get $0.08 back

    • Avatar Michael James on May 15, 2019 at 4:41 pm

      Hi Jay, we updated the post to include Rays code and his design write up. I hope this helps!

Leave a Comment