Charles van't Westeinde
Published © GPL3+

Solar Hot Water diverter with automatic boost

Heats a hot water storage tank using excess solar energy, topping up with off-peak imported power when needed.

IntermediateFull instructions provided1,449
Solar Hot Water diverter with automatic boost

Things used in this project

Hardware components

MCP9701A-E/TO THERMISTOR, LINEAR 19.5MV/C, TO92;
×1
Through Hole Resistor, 120 kohm
Through Hole Resistor, 120 kohm
×1
Electrolytic Capacitor, 470 µF
Electrolytic Capacitor, 470 µF
×1
Espressif nodemcu 12e
Assuming you will be sampling both power and temperature on the same chip, a nodemcu esp32 is probably a better choice, as the 12e has only a single ADC input.
×1
Through Hole Resistor, 10 kohm
Through Hole Resistor, 10 kohm
×1
General Purpose Transistor NPN
General Purpose Transistor NPN
×1
CARLO GAVAZZI RGC1A23D25KKE SOLID STATE CONTACTOR, 25A, 24VAC-240VAC
×1
ac/dc pcb mount power supply 5V
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Solar Diverter schema

Code

Library code

C/C++
All (and possibly a few more) of the code libraries supporting the main .ino file.
No preview (download only).

Solar Diverter + reporting

C/C++
Main .ino file containing the code for the Solar Diverter as described. Additionally implements a webserver providing a graphical representation of temperature and power history.
/*******************************************************************************
*
*	Obtain hotwater tank temperature, time-of-day and current power export.
*	Send excess solar power to the hotwater tank. Boost if necessary to reach
*	the daily target temperature.
*
*******************************************************************************/

#include <sys/time.h>                   // struct timeval
#include <coredecls.h>                  // settimeofday_cb()
#include <math.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <timeZone.h>
#include <signalSmoothing.h>
#include <ntoa.h>
#include <timeToString.h>
#include <stringBuilder.h>
#include <xmlBuilder.h>
#include <bugutil.h>
#include <wifiUtil.h>
#include <flash.h>
#include <bugutil.h>
#include <interpolate.h>
#include <keyvalue.h>
#include <svgTimeGraph.h>
#include <nmcu_DiverterGraph.h>
#include <warby_macros.h>
#include <math2.h>






///////////////////////////// Pin Assignment //////////////////////////////////////
//
//	source: https://zoetrope.io/tech-blog/esp8266-bootloader-modes-and-gpio-state-startup
//
//									GPIO_0 	GPIO_2 	GPIO_15
// UART Download Mode (Programming) 0 		1 		0
// Flash Startup (Normal) 			1 		1 		0
// SD-Card Boot 					0 		0 		1
//
// When choosing GPIO pins to use, its best to avoid GPIO 0, 2 and 15 unless
// absolutely necessary. If you do end up using them, you'll need 
// pullups / pulldowns to ensure the correct bootloader mode. 
// You should also be aware of the fact that GPIO 0 is driven as an output
// during startup (at least with NodeMCU).
//
//  source: https://community.blynk.cc/t/esp8266-gpio-pins-info-restrictions-and-features/22872
//  GPIO  function  ESP8266  notes / restrictions default behaviour on boot recc. for sensors for flash for run
//  0 I/O, CS for normal run, must be HIGH during boot / reset (has built in pull-up) oscillates, than stabilizes HIGH after ~100ms NO  LOW HIGH
//  1 TXD reserved for frimware upload and serial monitor. not reccomended as GPIO pin  LOW for ~50ms, then HIGH  NO    
//  2 I/O built-in led (active LOW). must be HIGH during boot / reset / wakeup (has built in pull-up) oscillates, than stabilizes HIGH after ~100ms NO  HIGH  HIGH
//  3 RXD reserved for firmware upload and serial monitor. not reccomended as GPIO pin  LOW for ~50ms, then HIGH  NO    
//  4 I/O, SCL  safe pin (first choice) LOW YES   
//  5 I/O, SDA  safe pin (first choice) LOW YES   
//  6 - 11    these pins are reserved for mcu - flash communication, do not use!    NO    
//  12  I/O, MISO (SDO) quite safe pin  HIGH for ~100ms, then LOW YES   
//  13  I/O, MOSI (SDI) quite safe pin  HIGH for ~100ms, then LOW YES   
//  14  I/O, SCK  quite safe pin  HIGH for ~100ms, then LOW YES   
//  15  I/O must be LOW during boot / reset / wakeup (has built in pull-down) LOW NO  LOW LOW
//  16  I/O (no interrupt)  for sleep mode, connect to EXT_RSTB, on wakeup will output LOW for reset  HIGH for ~100ms, then falls to ~1V? NO    
///////////////////////////// Pin Assignment //////////////////////////////////////
//

// Defining available LED's here, because the value of LED_BUILTIN has been known change with updates.
#define LED_1			16		// D0, WAKE
#define LED_2			2		// D4, TXD1




/////////////////////////// time ////////////////////////////////////////////

static bool cbtime_set = false;	// Do we have a valid time-of-day?

static void time_is_set(void) {
  cbtime_set = true;
}



static void setup_ntp(void)
{
  settimeofday_cb(time_is_set);
  configTime(0, 0, "pool.ntp.org");
}



/////////////////////////// Power declarations ///////////////////////////////


const int8_t 	analogInPin = A0;		// I: Reads measured or thermoStat temp
const int8_t 	ssrControlPin = D1;		// O: SSR control HIGH = "ON".

static	os_timer_t		ssr_control_timer;
	// Generates a PWM control voltage for the SSR.
	// Turn power on for nDivertCycles out of MaxCycles.

// PWM constants:

// Length of a cycle (msec)
#define	CycleLength		20

// PWM period length in cycles.
#define PwmCycles		36

// Load power
#define	Load			WARBY_HWS_LOAD


//Average Power contributed by every "on" cycle
// within the PWM period. (=100W)
#define PPP	(Load/PwmCycles)

// Target power range: try to get as close to "0" as possible
// dipping into import now and then should be more than compensated by
// minimising export.
#define MinExport			0
#define MaxExport			PPP

// Target temperature to aim for by TargetTOD
#define TargetTemp			67

// Number of degrees the actual temp lags behind desired to invoke full power
#define TempTolerance		2

// Temp just above where the thermostat kicks in
// In between TargetTemp and MaxTemp we limit power to prevent the mechanical
// thermostat from kicking in before the heat has been equalised within
// the tank.
#define MaxTemp			69

// Time-of-day after which only solar power should be used (secs)
// (start of peak tariff)
#define TargetTOD		(15 * SECS_PER_HOUR)


// Estimated temperature curve under full power.
// temp_curve[0] = final temp.
// temp_curve[1] = temp with 1 hr to go, etc.
#define	temp_curve_count	9
static const uint8_t	temp_curve[temp_curve_count] =
		{ TargetTemp, TargetTemp, 62, 57, 50, 40, 28, 14, 0 };

// Cycles per period to divert excess power.
static int16_t		nDivertCycles;

// Max cycles per period to prevent the thermostat from triggering early.
static int16_t		nLimitCycles;

// Power required to reach <minTemp> by <TargetTOD>.
static int16_t		nBoostCycles;

// The amount of power overload in terms of duty cycles.
static int16_t		nOverloadCycles;

// The final calculated number of cycles.
static int16_t		nCycles;


static uint32_t		nextTargetUTC;	// Next target time
static LowPass<1>		waterTemp(5);	// last recorded water temp.
										// rolling average over 5s to compensate
										// for ADC jitter.

static int16_t		currentImport;	// last recorded power import
static bool			haveImport = false;		//  <currentImport> is valid.


static ESP8266WebServer server(80);    	// Create a webserver object that listens for HTTP request on port 80

void handleRoot();              // function prototypes for HTTP handlers
void handlePage();				// Request for one of the graphs
void handleNotFound();

/////////////////////////// connections //////////////////////////////////////



// (local) service providing current "whole house" power import/export
//#define HTTP_TARGET	"60scotch.hopto.org:9200"
#define HTTP_TARGET	"10.0.0.122:80"
//static WifiManager	wm( "CnM", "isnochys", 15 );
static WifiManager	wm( WARBY_WIFI_SSID, WARBY_WIFI_PW, 300 );





static float minDesiredTemp( const uint32_t secsToHeat )
	// Minimum desired water temperature depening on how many seconds heating
	// time left.
{
	const uint32_t	wholeHoursToHeat = secsToHeat / 3600;
	
	if( wholeHoursToHeat >= (temp_curve_count -1) )
		// We're still before the start of the curve, return the lowest value
	{
		return temp_curve[temp_curve_count -1];
	}
	
	const uint32_t	wholeHoursToHeatSecs = wholeHoursToHeat * 3600;
	const LinearIP	ip(0.0,  temp_curve[wholeHoursToHeat], 3600.0,  temp_curve[wholeHoursToHeat +1] );

	return ip.y( secsToHeat - wholeHoursToHeatSecs);
}


static void updateOverloadCycles(  const int16_t curPower )
// Adjusts the reduction in power-cycles needed to limit overload.
// Increases immediately in case of overload, decrements every cycle if there is no overload.
{
	if( curPower > WARBY_POWER_LIMIT )
	{
		nOverloadCycles += 1 + (( curPower - WARBY_POWER_LIMIT ) / PPP);
	}
	else
	if( nOverloadCycles > 0 )
	{
		nOverloadCycles--;
	}
}



static void updateBoost(const uint32_t	utc, const float curTemp, const int16_t curPower)
	// Check if it is still possible to reach <MinTemp> by <TargetTOD> at full power. 
	// If not, start increasing power from 0 to full over 15 mins.
	// Effectively we'll reach <MinTemp> between <TargetTOD> and <TargetTOD+15m>
{

	const uint32_t	localTime = ae.toLocal( utc );
	
	const uint32_t	nextTargetLocal = nextTimeOfDay( localTime, TargetTOD );
		// Time until which we may import power.
		
	const uint32_t	timeRemaining = (nextTargetLocal - localTime);
	
	// Now calculate the temperature we should be at, given the remaining heating time.	
	
	const float	targetTemp = minDesiredTemp( timeRemaining );		

	const LinearIP	ip(targetTemp -TempTolerance,  PwmCycles, targetTemp,  0.0 );

	nBoostCycles = ip.yc( curTemp );	

	Serial.printf
	(	"waterTemp=%.2f, targetTemp=%.0f, nBoostCycles=%.2d\n",
		curTemp,
		targetTemp,
		nBoostCycles

	);			
}

static void updateCycles( void )
//	Combines all the calculated cycles into a single number.
{
	nCycles = nBoostCycles;		// Always boost if needed.
	
	if( haveImport && ( nDivertCycles > nCycles ) )
		// Increase power if we have excess solar power
	{
		nCycles = nDivertCycles;
	}
	
	
	if( nCycles > nLimitCycles )
		// Limit power to prevent the mechanical thermostat from kicking in early.
	{
		nCycles = nLimitCycles;
	}
	
	// Reduce power if there is any overload.
	nCycles -= nOverloadCycles;
}


static void ssr_control_fun( void * )
// Runs on a 20msec timer, i.e. once every cycle.
// counts cycles, turns off power after the specified # of cycles,
// resets at the end of the PWM period.
{
	static int16_t		iCurrentCycle;	// Current cycle counter.
	

	
	
	// Turn on SSR until we have reached nDivertCycles, or when we need to boost.
	digitalWrite( ssrControlPin, iCurrentCycle < nCycles );

	
	// Give each cycle a number 0..PwmCycles-1
	iCurrentCycle = (iCurrentCycle+1) % PwmCycles;
}


void setup()
{
	pinMode(LED_1, OUTPUT);
	pinMode(ssrControlPin, OUTPUT);

	Serial.begin(115200);
	Serial.println();

	Serial.println();
	Serial.println();
	Serial.println();

	for (uint8_t t = 4; t > 0; t--) {
		Serial.printf("[SERIAL] WAIT %d...\n", t);
		Serial.flush();
		delay(1000);
	}
	Serial.print(F("compile date: "));
	Serial.print(F(__DATE__));
	Serial.print(F(" "));
	Serial.println(F(__TIME__));
	
	setup_ntp();
	
	WiFi.mode(WIFI_STA);  // wifiClient
	
	server.on(F("/"), handleRoot);     			// Call the 'handleRoot' function when a client requests URI "/"
  	server.on(F("/page"), handlePage);     		// Return the specified page
	server.onNotFound(handleNotFound);        // When a client requests an unknown URI (i.e. something other than "/"), call function "handleNotFound"

	server.begin();                           // Actually start the server
	Serial.println(F("HTTP server started"));

	os_timer_setfn(&ssr_control_timer, ssr_control_fun, NULL );
	os_timer_arm( &ssr_control_timer, 20, true );// 20sec, or 1 phase.
}



float units_to_degrees( const int16_t units )
	// Converts ADC units into degrees C.
	// linear approximation from MCP9701 (thermometer) specs
{
	const float	V0C		= 0.4;					// 400 mV at 0 degree C.
	const float TC		= 0.0195;				// 19.5 mV / degree C.
	
	const float R1		= 119000;				// Voltage divider to ADC R2/(R1+R2)
	const float R2		= 101000;
	const float F		= R2/(R1+R2);
	const float UpV		= 1024;					// ADC units/Volt
	const int16_t zeroC = V0C * F * UpV;		// ADC units at 0C
	const float	dpu		= 1.0/(TC * F * UpV);	// degrees / unit
	
	const float temp =  dpu*(units - zeroC);
	
//	Serial.printf("ADC=%d, temp=%.2f\n", units, temp);
	return temp;
}




static void updateDivertCycles( const int16_t exp )
	// Updates nDivertCycles to match spare power.
{

	if( exp < MinExport )
		// Not exporting enough: reduce power.
		// To prevent a feed back loop, reduce power slowly for smaller differences,
		// but still fast for larger power mismatches.
	{
		nDivertCycles -= M2::max( 1, ((MinExport - exp)/PPP) -2);
		nDivertCycles = M2::max( nDivertCycles, (int16_t)0 );
	}
	else
	if( exp > MaxExport )
		// To prevent a feed back loop, increase power slowly for smaller differences,
		// but still fast for larger power mismatches.
	{
		nDivertCycles += M2::max( 1, ((exp - MaxExport)/PPP) -2);
		nDivertCycles = M2::min( nDivertCycles, (int16_t)PwmCycles );	
	}
}

static void updateLimitCycles( const float curTemp )
	// Limit the duty-cycle when water temperature exceeds TargetTemp
{
	static LinearIP	tempToCycles( TargetTemp, PwmCycles, MaxTemp, PwmCycles/8 );
	
	nLimitCycles = (int16_t) tempToCycles.yc(curTemp);
}


// For testing.
//#define		POWER_ADJUST( x )	((x) + (PPP*nTargetOnCycles))
#define		POWER_ADJUST( x )	(x)

//#define		RequestURL	F("http://60scotch.hopto.org:9200/read")
#define		RequestURL	F("http://"HTTP_TARGET"/read")




static bool updateCurrentImport(void)
	// Gets the current power-import from "fourquadrant_power"
	// Any kind of failure causes this function 
	// to report no power-data is available.
	// This causes the load to be turned off
{
	WiFiClient client;
	HTTPClient http;



	if (!wm.connect()) 
	{
		return false;
	}
		// We are connected to WiFi, get a power update.

	Serial.print("[HTTP] begin...\n");
	if (http.begin(client, RequestURL)) 
	{
		// start connection and send HTTP header
		int httpCode = http.GET();

		// httpCode will be negative on error
		if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) 
		{
        	char	s[128];

        	strcpy(s, http.getString().c_str());

        	const char	*value;

        	if( getKValue( s, "avpower", &value ) )
				// Got a value, adjust
        	{
				currentImport = POWER_ADJUST(atoi(value));

				return true;
        	}
        	else
        	{
        		Serial.println(F("Key not found"));
        	}
		} 
		else 
		{
			Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
		}
		http.end();
	} 
	else 
	{
		Serial.printf("[HTTP] Unable to connect\n");
	}
	return false;
}

///////////////////////////// WebServer declarations ///////////////////////////////////














///////////////////////////// Web Display //////////////////////////////////////

#define	STRINGBUF_SIZE	(2*8192)




class PageBuilder : private XMLBuilder
// Constructs page via the constructor,
// inheritance reduces the amount of typing needed.
{
  private:
    const TimeSeries		*pActiveChart;
	
    void button(FSH text, const int8_t iPage, FSH style )
    {
	  if( iPage < 0 )
	  // no button
	  {
	  	return;
	  }
	  
	  char url[16];

	  StringBuilder button_url_builder( url, 16 );

	  button_url_builder += F("/page?i=");
	  button_url_builder += iPage;
	  
      tag(F("p"));
      	tagStart(F("a"));
      		keyValue(F("href"), url);
      	tagEnd();
      		tagStart(F("button"));
      			keyValue(F("class"), style);
      		tagEnd();
      			value(text);
      		nodeClose();
      	nodeClose();
      nodeClose();
    }

  public:
    void build
    (	const TimeGraph *const	pActiveChart,	// Graph to build a page for
    	int8_t					iPageLeft,		// Page the left button points to, or -1
    	int8_t					iPageRight		// Page the right button points to, or -1
     )
    {
      dbprint(F("entering PageBuilder::build: chart="));
      dbprintln(pActiveChart->config.title);
	  
      writer.clear();

      tagStart(F("html"));
      	field(F("lang=en-AU"));
      tagEnd();
      	tag(F("head"));
      	  tagStart(F("meta"));
      		  keyValue(F("http-equiv"), F("refresh"));
      		  keyValue(F("content"), pActiveChart->config.refreshInterval);
      	  nodeClose();
      	  node(F("title"), F("Solar Diverter"));
      	  writer += F("<!DOCTYPE html>");
      	  tag(F("head"));
      		tagStart(F("meta"));
      		  keyValue(F("name"), F("viewport"));
      		  keyValue(F("content"), F("width=device-width, initial-scale=1"));
      		nodeClose();
      		tagStart(F("link"));
      		   keyValue(F("rel"), F("icon"));
      		   keyValue(F("href"), F("data:,"));
      		nodeClose();
      // CSS to style the left/right buttons
      // Feel free to change the background-color and font-size attributes to fit your preferences
      		tag(F("style"));
      		   write(F("html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}"));
      		   write(F(".button { position:fixed; z-index:2; background-color: white; opacity:0.5; border: 2px solid; border-radius: 12px; color: black; padding: 4px 8px;"));
      		   write(F("text-decoration: none; font-size: 30px; margin: 4px 2px; cursor: pointer; top:13%} "));
      		   write(F(".left {left:5%} "));
      		   write(F(".right {left:90%} "));
      		nodeClose();
      	  nodeClose();
      	  tag(F("style"));
      		 write(F("body { background-color: #fffff; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; font-size: 16px } "));
      		 write(F("svg { position:fixed; z-index:1; top:0; left:0; height:100%; width:100% }"));
      	  nodeClose();
    	nodeClose();
    	tag(F("body"));
      	  tag(F("hl"));
      		writer += pActiveChart->config.title;
	  		{
	  			pActiveChart->resetNextLine();
				while( pActiveChart->getNextLine( &writer ) );
	  		}
      	  nodeClose();

      	  SvgTimeGraph c(writer);
      	  c.build(*pActiveChart);

    	// Buttons
		  button(F("<<"), iPageLeft, F("button left") );
		  button(F(">>"), iPageRight, F("button right") );
    	nodeClose();
      nodeClose();
      
      Serial.println(F("leaving PageBuilder::build"));
    };

    PageBuilder( StringBuilder &writer ) :  XMLBuilder(writer) {};
};


/////////////////////// Request Handler implementations ///////////////////////////


static const char p1h[] PROGMEM = "Solar Diverter (1 hr)";
static const char p3h[] PROGMEM = "Solar Diverter (3 hrs)";
static const char p12h[] PROGMEM = "Solar Diverter (12 hrs)";
static const char p48h[] PROGMEM = "Solar Diverter (48 hrs)";
static const char p1w[] PROGMEM = "Solar Diverter (7 days)";

class HHMMSS_Label :  public TimeGraphXtickLabel
	// Writes the parameter as UTC HH:MM:SS
{
	void label( const int32_t v, StringBuilder *const pBuilder ) const
	{
		pBuilder->hhmmss(v);
	}
};
static HHMMSS_Label	hhmmssLabel;

class HHMM_Label :  public TimeGraphXtickLabel
	// Writes the parameter as UTC HH:MM
{
	void label( const int32_t v, StringBuilder *const pBuilder ) const
	{
		pBuilder->hhmm(v);
	}
};
static HHMM_Label	hhmmLabel;

class HHMM_DDMMM_Label :  public TimeGraphXtickLabel
	// Writes the parameter as UTC HH:MM - MMM DD
{
	void label( const int32_t v, StringBuilder *const pBuilder ) const
	{
		pBuilder->hhmm(v);
		pBuilder->write(F(" - "));
		pBuilder->ddmmm(v);
	}
};
static HHMM_DDMMM_Label 	hhmm_ddmmmLabel;


static TimeGraphConfig cfg1h  = {FPSTR(p1h), 180, 3600, 30, 6, 2, &hhmmLabel };
static TimeGraphConfig cfg3h  = {FPSTR(p3h), 180, 3*3600, 60, 6, 6, &hhmmLabel };
static TimeGraphConfig cfg12h = {FPSTR(p12h), 144, 12*3600, 150, 6, 4, &hhmmLabel };
static TimeGraphConfig cfg48h = {FPSTR(p48h), 144, 48*3600, 300, 4, 4, &hhmmLabel };
static TimeGraphConfig cfg1w = {FPSTR(p1w), 168, 7*24*3600, 600, 7, 4, &hhmm_ddmmmLabel };

static DiverterGraph    power1h = DiverterGraph( cfg1h );
static DiverterGraph    power3h = DiverterGraph( cfg3h );
static DiverterGraph    power12h = DiverterGraph( cfg12h );
static DiverterGraph    power48h = DiverterGraph( cfg48h );
static DiverterGraph    power1w = DiverterGraph( cfg1w );

static DiverterGraph  *apd[] = {&power1h, &power3h, &power12h, &power48h, &power1w, NULL};

void sendPage( const int8_t iPage )
{
  digitalWrite(LED_1, LOW);
  
  Serial.print(F("SendPage "));
  Serial.println(iPage);
  
  char *htmlBuffer = new char[STRINGBUF_SIZE];          // output buffer where we construct the message to send.
  StringBuilder  htmlBuilder(htmlBuffer, STRINGBUF_SIZE);   // basic string construction methods

  PageBuilder  c( htmlBuilder );
  
  c.build(apd[iPage], apd[iPage+1]!=NULL ? iPage+1 : -1, iPage -1 );
  	
  server.send(200, F("text/html"), htmlBuffer);
  
  delete [] htmlBuffer;
  
  digitalWrite(LED_1, HIGH); 
}

void handleRoot() {                         // When URI / is requested, send a web page with on/off buttoms showing current state
  Serial.println(F("handleRoot"));
  sendPage(2);
}

void handlePage() {                          // If a POST request is made to URI /WAVE
  Serial.print(F("handlePage "));
  Serial.println(server.uri());

  sendPage(server.arg(0).toInt());
}


void handleNotFound() {
  Serial.print(server.uri());
  Serial.println(F(" Not found"));
  server.send(404, F("text/plain"), "404: Not found"); // Send HTTP status 404 (Not Found) when there's no handler for the URI in the request
}


///////////////////////////// Main Loop //////////////////////////////////////



static float heatingPower(void)
	// Calculates heating power based on global variables.
{

	return PPP * nCycles;

}

void loop(void) {

	server.handleClient();   // Listen for HTTP requests from clients
	
	const uint32_t	utc = utcTime();
	
	static uint32_t		lastTempSampleTime = 0;
	
	if( utc > lastTempSampleTime )
	{
		waterTemp.nextSample(units_to_degrees(analogRead(analogInPin)), 1);
		lastTempSampleTime = utc;
	}


	static uint32_t		nextLoopTime = 0;
	
	if( utc < nextLoopTime )
		// wait until the appointed time for the next
		// processing loop.
	{
		return;
	}
	
	
	digitalWrite(LED_1, LOW); 
	


	updateLimitCycles( waterTemp.curValue() );
	
		
	haveImport = updateCurrentImport();

	if (haveImport) 
		// We have a valid import value: adjust diversion power if needed.
	{
		updateDivertCycles( -currentImport );
		updateOverloadCycles( currentImport );
		
		// Next loop in 10 sec.
		nextLoopTime = utc + 10;
	}
	else
		// http call failed. Try again in 2 sec.
	{
		nextLoopTime = utc + 2;
	}
	
	if( cbtime_set )
		// Check if we need to go to max power in order
		// to get the water to TargetTemp by TargetTOD.
	{
		updateBoost(utc, waterTemp.curValue(), currentImport);	
	}
	else
		// We don't know the time. Half power irrespective of time
		// until TargetTemp
	{
		nBoostCycles = (waterTemp.curValue() < TargetTemp) ? (PwmCycles / 2) : 0;
		Serial.println("[NTP] WAIT");	
	}

	updateCycles();
	
	for( int i = 0; apd[i] != NULL; i++ )
	// 	feed the graphs.
	{
		apd[i]->addSample(waterTemp.curValue(), currentImport, heatingPower(), utc );
	}
	
	
	Serial.printf
	(	"haveImport=%d, import=%d, temp=%.2f, divertCycles=%d, limitCycles=%d, boostCycles=%d\n",
		haveImport,
		currentImport,
		waterTemp.curValue(),
		nDivertCycles,
		nLimitCycles,
		nBoostCycles			
	);			
	digitalWrite(LED_1, HIGH); 
		
}

Credits

Charles van't Westeinde

Charles van't Westeinde

3 projects • 1 follower
Electronics engineer by education, software engineer by trade. Combining the two is my idea of fun.

Comments