Distributed Solar Divert

(Small Powerwalls, Basically)

Do you have solar on your house? Well, I've got the DIY project for you!

The problem: A powerwall might be cool, but it's the wrong answer for most solar houses.

So, if you have solar installed already, then you know it's the beginning of the journey, not the end. If you're like me, you're monitoring every kWh of power you consume in your house and have become obsessed with it. It really is an obsession. You've done all the insulation, upgrades, and efficiency checks necessary to get your power consumption under the Solar power you are producing. Powerwall's "sound" cool, but they cause more problems than they're worth:

  1. You have no "serious" control over the lifespan of the batteries or the long-term cost of replacing them. Powerwalls are appliances. Not a real solution.
  2. A powerwall provides you with A/C power to your entire home, unless you're smart enough to install one yourself, when in reality the power you use at night when the sun is not shining is probably a fraction of what you use during the day. If you've had solar for long enough, you know that you want to use as much of the power you generate when you generate it => That's the most cost effective way to stop pulling expensive power from the grid at night.
  3. You have fine-tuned your home. (Perhaps you even have zoned air conditioning). You're really good at time-of-use billing, and you know how to make sure that your appliances are using power at the right time of the day.

If that's true, then you likely don't need the entire 13(-ish) kWh that a Powerwall can provide you with everyday => You probably need a lot less. In these circumstances, even though the powerwall has an AMAZING price-per-kWh ratio compared to buying the batteries yourself (due to economies of scale), you don't actually need that much stored power. So, instead, what if you had a solution that was significantly scaled down? => A solution that you could deploy PER APPLIANCE, and not be dedicated to the whole house? how cool would that be?


  1. My home NAS and surveillance system uses 5 kWh in a single 24-hour period.
  2. Each room in my house uses only 0.8 kWh for Air conditioning each night (using zoned mini splits).
  3. My electric water heater is on a timer => It doesn't even run at night in the first place.
  4. The electric car is on a timer => It can charge during the daytime (or whenever I open my phone and hit the charge button). Piece of cake.

What does that mean? A powerwall is overkill. It's a hammer, when what we really need is a butter knife. We need a DISTRIBUTED solution. A per-room or per-appliance solution for storing energy. Not a whole-house solution.

At night time, I'm using at most 3-4 kWh in the ENTIRE house, and that power usage is confined to specific appliances in the home. I don't need a powerwall for that. Instead of forking over $7K for a powerwall (more like $9-10K including taxes and installation costs), I really only need about $3K worth of batteries (and some DIY blood, sweat, and happy tears) and I can still end up with a storage solution at half the price. Not only that, when a specific group of batteries dies (in a few years, most likely), I need only to recycle those batteries and get new ones. No need for a phone call. The costs savings add up pretty quickly over the course of 20-30 years........ even more so considering how fast the price of lithium technology is dropping.

How do you do build such a thing? Well, let's make one, of course!

The Design:


The main hurdle in build a distributed storage system is that you want this thing to be plug in play, so there a few problems to solve here:

  1. NO NEW ELECTRICAL WIRING in the home should be required at all to do this. None. Zip. Zippo. Nada. If we have to call in an electrician to do this project, then, game over. It's too complex for a DIY project and it ain't gonna work. We want to get our power from Solar, but we don't want to have to run any new Romex (google it) around the house just to make it happen. So, that means we have to do an A/C to D/C conversion from the Wall, and that's what we'll be doing.
  2. Any batteries should work. (Lithium, Lead-acid, AGM, whatever). In a sense what we're going to be building is a BMS (battery management system), but an internet-connected BMS that diverts solar power into the batteries for you. If you want to start out with a cheap $100 Deep-cycle battery to prove that everything "works" and then upgrade to Lithium, you should be able to do that. If you want a dozen lead-acid batteries in your backyard, well, hell, you should be able to do that to! We should not have to be constrained to a specific battery technology for this to work.
  3. Unless you're using Lithium-Phosphate batteries (I use both Lithium AND Lead-acid), Ideally, you don't want Lead-Acid batteries inside your house (in case of battery failure). So, as you're proceeding with this project, if there is not an outside outlet your house to place the finished product, then I'm assuming you can run an extension cord to get the A/C power from where you need it. In my case, the whole system ran inside a shed and also outside in the backyard powering a couple of bedrooms. This can be tricky depending on your circumstances. For my bedroom, yes, I had to run some Romex through the wall to get to the storage system (because of the air conditioner) => So, maybe I kind of lied in point #1. But that's really an easy problem to solve. I only needed to upgrade a single outlet in that case. It was not necessary to rewire my ENTIRE house to make it work. I simply extended an inside outlet to become an outside outlet on the same side of the house. That's a really simple DIY job.
  4. Failures in individual components should be easy to handle and we should be notified if failures occur in the different componets of our DIY system. Failures in any individual component should not result in a failure of the appliance to get power => The power should always be available from the grid, even if the system fails while it's running over the years.

High-level Design Circuit:

Steps in the design (above):

So, as I said, we're basically building an internet-connected BMS (battery management system), which works like this:

  1. A device (an Arduino in my case) knows what time of day it is. I have raspberry pis in my home for other purposes, but I wanted this one to be Arduino for reliability purposes. We don't need Linux for this and we don't need to worry about the device being a botnet, so just use something simpler.
  2. If the sun is shining, we trip a "Transfer Switch" (common terminology) using a simple Relay. When the system is offline, A/C power from the wall just goes directly to the device you are trying to power and nothing happens. When the system is online (batteries are full), the switch trips: It redirects power from the batteries to your device instead of from the wall. That's what a transfer switch is. It transfers power from one circuit to another circuit.
  3. When the sun is shining, the transfer switch doesn't trip. It continues pulling power from the Sun to power both the device and store power in the batteries simultaneously. (Depending on the size of your battery bank, you need to make sure that you have enough power to run both the appliance *and* charge the batteries at the same time, so be careful).
  4. When the batteries are charged, the transfer switch waits for the sun to stop shining.
  5. When the sun stops shining, the transfer switch activates. The appliances now begin pulling power through an inverter connected to the batteries.
  6. When the batteries are empty, the Arduino detects this and makes the transfer switch trip again. If the sun is still not shining yet when this happens, you get switched back to grid power. Since we're connected to the Wall A/C, we don't really care => It's a Grid-tied Solar system, so our "circuit" need not worry about that. It needs only to know if the sun is shining and whether or not we are producing a sufficiently large amount of power to cover our needs.
  7. When the sun in shining again, the transfer switch remains the same, but we now need to activate the battery charger while the switch remains tripped. The Arduino later detects when the batteries are fully charged. Then we go back to step #2 and repeat everything all over again for the next 24 hours.

Fun, right? So, as you can imagine, this turns out to be a rather complex state machine. In fact, writing the software to run the state machine is the hardest part of the whole project. The wiring is "mildly" difficult, but you can do it! It's fun. =)

The State Machine:

So, what does that state machine look like? Well, here is what I figured out during the "research" phase of all this:

Background Research that I went through:


As you can see above, the hardest part in building a state machine above for diverting solar power to the batteries is tracking the voltages of the battery and the battery charger. The Arduino has to work in lock-step, basically over the entire lifecycle of the battery, and it has to be VERY precise. If the voltage gets too low, you can kill the batteries. If the voltage gets too high for too long, you can kill the batteries. Killing the batteries is very easy. =) To avoid over-charging the batteries, I decided to use an off-the-shelf battery charger and simply let it do the over-voltage detection by itself, but we still have to track the voltages very carefully to know WHEN the charger has completed charging. Lithium batteries charge very fast, but Lead batteries charge very slow. Lead voltages drop very steadily, linearly. Lithium voltages really only start dropping when the battery is nearly empty (like the battery is trying to play a trick on you). It's very tricky, but it's doable.

NOTE: The above diagram changes slightly for Lithium-Phosphate batteries. The voltage ranges are completely different. In the code below, I define the following voltage ranges using my own personal testing of both Lithium and Lead-Acid batteries.

Lithium batteries also do not have an "absorption" charging phase. The charger does trickle charge at the end of the charge, but it doesn't drop the voltage. Nevertheless, the state machine doesn't change that much, but the voltages do change.

#define MAX_CHARGING_VOLTAGE 14.95 // for a lead battery. Some chargers use voltages that are way too high. Be careful.
#define MAINTENANCE_MIN 13.1  // Lead: We try to detect when a lead-acid/AGM charger goes into "maintenance" or float mode.
#define MAINTENANCE_MAX 14.3 // Lead: 14.7 idle was observed, but charger wasn't finished
#define ABSORPTION_VOLTAGE 14.6 // This for lead, but Lithium also peaks it's charging voltage at this range too.
#define IDLE_FULL_VOLTAGE_LEAD 12.65 // Charger is done, battery is idle.
                                     // Probably more like 12.8, but I consider it usable anyway at 12.65 and above.
                                     // Charger is done, batter is idle.
                                     // 13.40 observed is about 98-99%. Full is well over 14 volts, but that's silly.
#define EMPTY_LEAD 11.65   // UNDER LOAD voltage, not idle. Represents about 50% state of charge.
                           // If you didn't already know this, you're not supposed to discharge Lead batteries more than 50%
#define EMPTY_LITHIUM  11.80 // UNDER LOAD voltage, not idle. Represents just under 10% State of charge.

The bottom line here is: You HAVE to perform your own load test of the battery you test. Every battery is different and operates on different voltage ranges. Even batteries from the same manufacturer or of the same type are different. Trust no-one.


Time is hard, but not that hard. =) The 2nd difficulty in designing the state machine is time. The code uses NTP for that. It updates the time of the day by contacting an NTP server once a day to account for drift. The Arduino does not have an on-board Real-Time clock, so getting the time from the internet is the only viable solution besides buying an RTC clock and adding it to your Arduino. That's not necessary since we want the device to provide us with logging and debugging feedback, so let's just use NTP for what it's good for. =) Second, the code assumes the sun is always shining at 8am and stops shining at 8pm. Obviously, that's not true, but lead acid batteries need a full 12-hour period to charge, so it's really the best we can do. This is ok as well, because it's good for time-of-use billing. As long as the "bulk" of the charge comes from Solar, the tail ends of the charge still come from the grid. This way, these early morning and late-night charging hours will be cheaper than charging at "peak" time when the power company changes the price of the electrons on you. Eventually, in the next version of this project I will actually reach out to the internet to figure out if it's cloudy... or figure out if the solar panels are actually producing enough power to charge the batteries in the first place. If you noticed above, we're using an energy monitor (a Sense energy monitor). If we could communicate with that service to get instantaneous information on the actual solar power production status at any given moment, then we can make much more accurate use of the system as a whole. But, for now, we just assume the Sun is always shining at specific hours of the day and work out the "finer" details later.


Voltage detection for batteries is very finicky. When a battery is fully charged, the voltage jumps around a lot. When you take the load off of the battery, the voltage ALSO jumps around a lot. When you place a load on the battery, you guessed it? It jumps around a lot. =) So the first thing you notice is that using a basic moving window average of the voltage (which is what we use in the code below) really isn't good enough. The voltage can jump around from anywhere from seconds to an hour, depending on what kind of battery you use. In order to handle this, you have to poll at different intervals while reading the voltage. You cannot simply assume that the voltage between any two time periods represents the "steady-state" voltage, even when the battery does not have a load on it. So, in the state machine above, we have the concept of "IDLE" and "NOT IDLE". Each state in the state machine tries as hard as possible to not change state unless the voltage is in "IDLE".

Finally, Idleness itself varies: Detecting when a voltage has stopped changing (e.g. it's Idle) must be done differently when the battery is under load or not under load. For example, if the battery is being actively charged, most chargers work by gradually increasing the voltage in very small increments (Ohm's law) over time. As the voltage increases by minute amounts (0.01v) increments, what time span do you poll for the voltage to determine that the voltage is idle? One minute? One hour? The only way to know that is to increase or decrease the polling interval incrementally until the delta between the last voltage and the current voltage stops changing. Only then, do you know if the voltage has actually reached an idle (steady) state. That also turned out to be very tricky when tracking the progress of the battery charger. But, quite fun. In these cases you want to poll slowly when the battery is in the bulk phase of charging, but you want to poll quickly when the battery is in the absorption phase. On the flip side, how about when the battery is under load? In these cases you want to poll the voltage much faster (every 10-30 seconds), especially for lithium batteries. The voltage curve of a lithium battery is very flat.... it's crazy. It almost doesn't change until the battery is completely empty, but we can still track it. Fun times. =)

Take a look at the above voltage curves. (The gaps in the lines don't mean anything: I was writing down the numbers by hand while looking at the monitor, so the gaps are just missing samples that I didn't record). In particular: NOTE that these curves are under a 400W load (the load that I'm expecting to pull from the batteries). It's really, really critical to perform a load test like this. The 115 amp-hour lead-acid battery was only drawn down to 50%, so I stopped around 0.6 kWH. Similarly the lead-acid battery was drawn down almost completely to just under 1.1 kWh. The Lithium voltage curve of a 100-amp-hour battery under load is really crazy: It's almost flat until the VERY end, so in our setup, we have to make sure that the Ardunio is paying very close attention to the voltage when the battery is nearly empty.

Final Product

Packaged Versions


Below, are the packaged versions of the whole project:

First, the lead-acid version. What you're seeing is:

  • A $120 Exide Marine Deep Cycle lead acid battery @ 115-amp-hours. It cost about $100-ish at Home Depot. It discharges very well and the amount of power closely matched the specifications of the battery. I actually bought 2 other new batteries and tested them and the manufacturers basically lied. They didn't tell the truth about the capacity of the battery, but this company was very good.
  • A $450-dollar A Samlex 1500-Watt DC/AC Pure Sine Wave Inverter (in the front of the black box)
    • It seems kind of silly to buy an inverter that's so expensive, but I originally bought a cheap $150-dollar chinese-made inverter, and it died within one day. I was pissed. So, since this thing is supposed to run for many years, I decided to just go ahead and buy the best thing I could find.
  • A $150 Samlex two-stage power charger (15amp DC)
    • Over the long-term, don't attempt to use an off-the-shelf cheapo battery charger unless you're just prototyping the system. For lead-acid, we don't use the absorption phase, for reasons I learned later in the "Mistakes" section, so you need a charger that allows you to control what charging phase you need.
  • A $45 Arduino Uno Wifi Rev2 with a $20 Relay Shield mounted on top of it.
  • Lots and lots of spliced of wires according to the aforementioned wiring diagram. =)

On the left-hand side is just a black box (no pun intended) that holds the battery, inverter, and charger. The Arduino is sitting on top.

On the right-hand side is a look inside the box showing them all sitting together.

Lithum-Phosphate Version:

So, I also made a lithium version of this with a 3-battery, (300 amp-hours) bank connected in parallel.

Below, you see:

  1. Another $450 Samlex 1500W Pure Sine Wave inverter, just as before (middle)
  2. A $160 Progressive Dynamics lithium battery charger (left). The charger should not have any buttons on it, it should be dedicated to the type of battery you're using.
  3. One of three $950 battle-born 100 AH batteries (right).
  4. The arduino is not in the picture, but it's there. I'll take another picture once it's fully assembled.

Mistakes that I made:

The mistakes were possibly the funniest parts of the whole project:

  • The funniest mistake I made was when the cheap Chinese inverter died. I was trying to save money, but it didn't work out. This presents a really difficult failure scenario. What do you do if the inverter dies? The solution I came up with was that when I bought the new inverter, I connect the power of the Arduino to the inverter itself. That way, if the inverter dies, the "transfer" Relays on the Arduino-controlled relay shield all default to an "off" (normally closed) position and the appliance drops back automatically to grid power, so that the system keeps running. "Detecting" when this happens is pretty easy: You need to have a log server of some kind (included in the code below). When the Arduino loses power, I have an email that comes to me saying that the Arduino is no longer "checking in" with me, then I know something is wrong and that I need to go replace the component that failed.
  • I also initially bought a shitty battery charger. Sure enough, just a few days after the crappy inverter died, the battery charger died. To add insult to injury, just before that happened, the battery charger came with those huge clamps instead of ring terminal adapters. Silly mistake. This caused problems delivering current to the battery and ended up causing terminal corrosion (white powder) at the terminal. If you're going to continuously charge a battery, you need excellent contact with the terminal. Be careful, that stuff is really toxic. =)
  • Another mistake I made was buying a bad battery which was new. You would think that new batteries should be good, but often they are not. When I did a load test of the battery, only got about 60% of the power the battery was rated for. Be careful!
  • Don't forget that DC amps != AC amps. This is really easy to forget. For example, the 30-amp rated (D/C) lithium charger that I use only draws 7 amps A/C! Confusing, right? That's because of the way the phases in A/C power work when the current is alternating (read about it). That's a huge difference. But, if the A/C outlet at the wall needs to charge the batteries *and* supply power to the appliance simultaneously while the sun is shining, then you really only have 7-8 amps of A/C power remaining to work with. BUT, even that is not true: You definitely do not want a 12-guage Romex (which is typically used for your average home's wall outlet) to be handling 15 amps of power continuously over many hours. Have you ever touched a wire that was handling that many amps continuously over many hours? It gets REALLY hot. And it's not just hot where you touch it.. it's hot in the walls everywhere the wire is located all the way to the circuit breaker. Just because a wire is "rated" handle that many amps over a short period of time doesn't mean it can handle that amperage over a long period of time. So, be really careful. This also puts a limit on the size of the battery bank you can use: In the same example, if you can only supply, say, 30 amps (D/C) of power to your batteries, a typical 100 amp-hour lithium battery takes 3-4 hours to bulk charge (not including the trickle phase at the end). Now, what happens if you have 3 or 4 of those batteries receiving the same amperage? Well, that's definitely not going to finish charging while the sun is shining. So, really the solution we have in this tutorial is limited to only a 200-300 amp-hours per circuit in your home (a circuit being one single circuit breaker in your home's electrical panel). If you need more power than that, you're going to have to duplicate the system for each room or outlet in the house => And that really is the purpose of the whole system: It's distributed, hence the title.
  • Do not buy an off-the-shelf battery charger at the hardware store. For three reasons:
    • This project connects your batteries permanently to an inverter. The inverter draws a small amount of charge to keep it running. This confuses the battery charger into thinking that the battery is not always full yet (particularly for lead-acid batteries), so the "Absorption phase" (which doesn't apply to Lithium batteries) never ends. This has the effect of holding the lead-acid battery over 14-volts (absorption) for a very, very long period of time => You're basically cooking your battery. You'll know you're cooking it because it actually sounds like it's cooking...... there's bubbling going on inside the cells and you can actually hear the acid turning into a gas right inside the case. You might think AGM would solve that, but you would still destroy the battery. This ends up causing terminal corrosion (powdery substances) at the terminals, it's just all around bad. The problem with skipping the absorption phase is that this kind of prevents your lead-acid batteries from reaching a true 100% charge. This really isn't a big deal because the batteries are so damned cheap. Safety first. On the other hand, what you could do is: Modify the circuit to connect a switch to your inverter (most inverters have a switching capability) so that the battery charger can read the voltages correctly and enter the absorption phase safely. I threw this option out as well because lead-acid batteries can take many minutes (up to an hour) for the voltage to "stabilize" after they have been under load. The voltage of a lead-acid battery must be idle (and the surface charge removed) for a long enough period for the voltage to stop fluctuating across all the cells. This basically means that in a multi-cell battery bank, it could take 1-2 hours for the whole battery bank to stabilize it's voltage........ in the meantime you can't charge the batteries! Which means you're burning daylight....... the state machine above doesn't work right. Lead-acid batteries really need a full 12-hour charge cycle to be effective or as much sun as you can give them. So, the best option really is to ditch the absorption phase and the Samlex charger I bought allows you to do that. Basically this is a type of "RV" battery charger, because RV batteries are also always under load, and basically have the same problem. (We are frequent RV-ers, so I was familiar with the problem). So, if you've got a project like this going to the point where it's going to be of regular use and you're using lead-acid, then that's the type of battery charger that you really want. The Samlex charger comes with a dip-switch that allows you to manually disable the absorption phase of the charger. (It also shows you the DC amperage right on the charger, which is super-cool).
    • Lithium charging is completely different from Lead-acid charging. For the most part, Lithium batteries do not leak charge like lead batteries do. Nor do they require a "float" charging phase to keep them "maintained", so conventional battery chargers don't apply. The good news is that it's highly unlikely for you to overcharge a "good" lithium battery because these usually come with very sophisticated internal BMS (battery management systems) built-into the battery to prevent you from doing that, which is good for a proof-of concept. But, over the long run, these batteries don't float, so you need a lithium-specific charger that takes the battery up to 14.6 volts (even when connected to the inverter) and then shuts off, basically without floating anything. The progressive dynamics charger that I bought (a lithium-specific version) does exactly that. Make sure you buy something like that.
    • The battery charger should not have any "on" or "off" buttons. If you plug it in, it should start charging immediately without delay, otherwise that makes the Arduino circuit too complex. Both the Progressive Dynamics and Samlex chargers above do that job very well for both Lithium and Lead acid. When the Arduio relay trips, charging begins immediately and there's nothing additional to be done by you.

Show Me The Code!

So, again, the code below was designed to run on an Arduino Wifi Uno Rev2 combined with a Relay Shield with 4 relays on it.

The code has a lot of functionality:

  • It connects to your local Wifi network.
  • It exposes an HTTP server for debugging purposes.
  • It logs to an rsyslog server on your local network so you can track how the battery voltages and state machine operates over time.
  • It implements the entire state machine in the picture above.
  • It supports both Lithium-Phosphate AND Lead-Acid batteries.
  • Check it out!

Example output from the HTTP port during the "Charging" state:

now: 6/27/2019 04:01 PM
We are: IDLE
Polling Interval: 2040
Seconds until next check: 1829
Last voltage: 12.44v
voltage: 12.46v
Current state: CHARGING 

Example output from the rsyslog server when the Arduino is switching to nighttime "Discharging" state:

Jun 26 23:44:39 arduino solardivert Changing DISCHARGING
Jun 26 23:44:39 arduino solardivert TO => DARK
Jun 26 23:44:40 arduino solardivert Turning AC on, Battery off.
Jun 26 23:44:40 arduino solardivert Turning charger off.

Example rsyslog configuration:

$ModLoad imudp
$UDPServerAddress // example
$UDPServerRun 514

# All logs from rsyslog code <167> go here, corresponding to one source arduino IP address
local4.* /var/log/solardivert.log
# All logs from rsyslog code <175> go here, corresponding to another source arduino IP address
local5.* /var/log/solardivert2.log

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

    Author: Michael R. Hines, https://michael.hinespot.com/tutorials/solardivert


#include <SPI.h>
#include <WiFiNINA.h>
#include <WiFiUdp.h>
#include <avr/wdt.h>

#define MAX_VALUES 30
// pin 4 == relay 4
// pin 5 == relay 3
// pin 6 == relay 2
// pin 7 == relay 1

// Hots J2 / Relay2
#define TransferControl1 6
// Neutrals J1 / Relay1
#define TransferControl2 7
#define ChargerControl 4
#define MAXLINE 40
#define UTCOFFSET 5 // central 

#define ON_BATTERY  0
#define OFF_BATTERY 1
#define CHARGER_ON  2
#define CHARGER_OFF 3


#define MAX_DIFF 0.05
#define SUN_START_LEAD 8UL // 8am
#define SUN_START_LITHIUM 9UL // 12am
#define SUN_STOP_LEAD 20UL // 8pm, we need at least 12 hours of charging time.
#define SUN_STOP_LITHIUM 19UL // 7pm
#define SUN_SHINING(date) (getHours(date) >= sun_start_actual && getHours(date) <= sun_stop_actual)
#define MAX_CHARGING_VOLTAGE 14.95 // according to exide specifications from website exide.com
#define MAINTENANCE_MIN 13.1
#define MAINTENANCE_MAX 14.3 // 14.7 idle was observed, but charger wasn't finished
#define IDLE_FULL_VOLTAGE_LITHIUM 13.35 // 13.40 observed
 * The actual voltage under HEAVY load is more like 11.65,
 * but I'm finding that devices that do not draw that much
 * power when a lead_acid battery reaches 50% are more likely
 * to reach 50% at a voltage slightly higher than 11.65
 * using a single battery. Perhaps a battery bank would
 * behave differently.
 * For lithium, just under 10% SoC is good enough, which has a different voltage.
#define EMPTY_LEAD 11.65 // SOC == state of charge
#define EMPTY_LITHIUM  11.80

#define LOGSIZE 150
#define LOG(foo)  Serial.println(foo); \

// WiFi Information
char ssid[] = "your_wifi_network";        // your network SSID (name)
char pass[] = "your_wifi_password";    // your network password (use for WPA, or use as key for WEP)

String rsyslogcode;
unsigned long sun_start_actual, sun_stop_actual;
float idle_full_voltage;
float empty_voltage;

bool success = true;
boolean on = false;
bool reached_maintenance_max = false;
boolean full = false;
boolean useHttpVoltage = false;
boolean idle = false;

int pollingInterval; // State machine hasn't started yet. Check often.
int httpHours = -1;
int count = 0;
int pingResult;
int pingWorks = 0;

float values[MAX_VALUES] = {0};
float voltage = 0.0;
float total = 0.0;
float httpVoltageReading = 12.5;
float last_voltage = 12.0;

WiFiUDP udp;
WiFiUDP udp2;
WiFiServer server(80);

unsigned long last_millis = millis();
unsigned long readVoltageElapsed;
unsigned long statusVoltageElapsed;
unsigned long checkVoltageElapsed;
unsigned long connectionCheckElapsed;
unsigned long lastNtpUpdate = 0;
unsigned long unixTime;
unsigned long currentIdle = 0;

   These are the 'Primary' charging modes. But these are not states in the state machine, they just indicate
   whether or not we should choose the default voltage polling intervals from the 1st group of 3 tunables above
   or the 2nd group of 3 tunables.
enum poll_types {

   Idle detection of the battery's voltage is critical for the state machine.
   Determining how often to check the voltage is not always the same, because
   the voltage moves at different rates when there is a load on the battery
   versus when the battery is at rest. This causes charging times to vary,
   and it causes variances in the time it takes for the battery voltage to
   stop changing when the battery is at rest. The only option here is to adjust
   the polling frequency on the fly. Below are "reasonable" guesses about
   how often to change the polling frequency based on live load tests of a
   standard lead-acid battery.

enum poll_const {

const int poll [NB_POLL_TYPES][MAX_POLL_CONST] = {
  [POLL_SLOW] = {
    [MAX] = 3600, // Full battery's voltage should be idle for at least an hour.
    [MIN] = 600, // Charging can be slow. We don't want to check more than every few minutes
    [INC] = 120 // move the frequency by 2 minutes at a time, when wrong.
  [POLL_FAST] = {
    [MAX] = 300, // A battery under load depletes very quickly. Check fairly often, even in the worst case.
    [MIN] = 10, // Under load
    [INC] = 60 // Don't decrease the polling interval too fast under load.

// Test poll intervals

const int debug_poll [NB_POLL_TYPES][MAX_POLL_CONST] = {
   [POLL_SLOW] = {
    [MAX] = 30,
    [MIN] = 5,
    [INC] = 2
  [POLL_FAST] = {
    [MAX] = 10,
    [MIN] = 2,
    [INC] = 1

enum state {  STATE_START, // 0
              STATE_CHARGING, // 1
              STATE_DISCHARGING, // 2
              STATE_DARK, // 3
              STATE_SOLAR, // 4
              STATE_ABSORPTION, // 5

state currentState = STATE_START;

// State Relay definitions
const char state_activations[NB_STATES][2] = {

const char * currentStateDescriptions[NB_STATES] = {

const char modes[] = {

typedef struct
  unsigned int year;
  unsigned char month;
  unsigned char day; unsigned char dayOfWeek;
  unsigned char hours;
  unsigned char minutes;
  unsigned char seconds;
  unsigned int milliseconds;
} DateTime;

enum state evalSolar(bool idle, DateTime *now);
enum state evalCharging(bool idle, DateTime *now);
enum state evalDark(bool idle, DateTime *now);
enum state evalDischarging(bool idle, DateTime *now);
enum state evalStart(bool idle, DateTime *now);
//enum state evalAbsorption(bool idle, DateTime *now);

int getHours(DateTime *dt) {
  if (useHttpVoltage) {
    return httpHours;

  return dt->hours;

   © Francesco Potortì 2013 - GPLv3 - Revision: 1.13

   Send an NTP packet and wait for the response, return the Unix time

   To lower the memory footprint, no buffers are allocated for sending
   and receiving the NTP packets.  Four bytes of memory are allocated
   for transmision, the rest is random garbage collected from the data
   memory segment, and the received packet is read one byte at a time.
   The Unix time is returned, that is, seconds from 1970-01-01T00:00.

unsigned long ntpUnixTime ()
  static int udpInited = udp.begin(123); // open socket on arbitrary port

  const char timeServer[] = "pool.ntp.org";  // NTP server

  // Only the first four bytes of an outgoing NTP packet need to be set
  // appropriately, the rest can be whatever.
  const long ntpFirstFourBytes = 0xEC0600E3; // NTP request header

  // Fail if WiFiUdp.begin() could not init a socket
  if (! udpInited)
    return 0;

  // Clear received data from possible stray received packets

  // Send an NTP request
  if (! (udp.beginPacket(timeServer, 123) // 123 is the NTP port
         && udp.write((byte *)&ntpFirstFourBytes, 48) == 48
         && udp.endPacket()))
    return 0;       // sending request failed

  // Wait for response; check every pollIntv ms up to maxPoll times
  const int pollIntv = 150;   // poll every this many ms
  const byte maxPoll = 15;    // poll up to this many times
  int pktLen;       // received packet length
  for (byte i = 0; i < maxPoll; i++) {
    if ((pktLen = udp.parsePacket()) == 48)
  if (pktLen != 48)
    return 0;       // no correct packet received

  // Read and discard the first useless bytes
  // Set useless to 32 for speed; set to 40 for accuracy.
  const byte useless = 40;
  for (byte i = 0; i < useless; ++i)

  // Read the integer part of sending time
  unsigned long time = udp.read();  // NTP time
  for (byte i = 1; i < 4; i++)
    time = time << 8 | udp.read();

  // Round to the nearest second if we want accuracy
  // The fractionary part is the next byte divided by 256: if it is
  // greater than 500ms we round to the next second; we also account
  // for an assumed network delay of 50ms, and (0.5-0.05)*256=115;
  // additionally, we account for how much we delayed reading the packet
  // since its arrival, which we assume on average to be pollIntv/2.
  time += (udp.read() > 115 - pollIntv / 8);

  // Discard the rest of the packet

  return time - 2208988800ul;   // convert NTP time to Unix time

float approxRollingAverage (float avg, float new_sample) {

  avg -= avg / (float) MAX_VALUES;
  avg += new_sample / (float) MAX_VALUES;

  return avg;

void readVoltage() {
  if (useHttpVoltage) {
    voltage = httpVoltageReading;
  } else {
    voltage = approxRollingAverage(voltage, (5.0 * ((float) analogRead(A0)) * (29800.0 + 7500.0)) / (1024.0 * 7500.0));

  if (!full) {
    if (++count == MAX_VALUES) {
      full = true;

void chargerOn() {
  LOG("Turning charger on.");
  digitalWrite(ChargerControl, LOW);

void chargerOff() {
  LOG("Turning charger off.");
  digitalWrite(ChargerControl, HIGH);

void offBattery() {
  LOG("Turning AC on, Battery off.");
  digitalWrite(TransferControl1, LOW);
  digitalWrite(TransferControl2, LOW);

void onBattery() {
  LOG("Turning AC off, battery on.");
  digitalWrite(TransferControl1, HIGH);
  digitalWrite(TransferControl2, HIGH);

void activateState(const char * activations) {
  if (currentState == STATE_CHARGING) {
    reached_maintenance_max = false;
    LOG("Resetting absorption detection. Setting reached_maintenance_max = false");
  if (activations[BATTERY_STATE] == ON_BATTERY) {
  } else if (activations[BATTERY_STATE] == OFF_BATTERY) {

  if (activations[CHARGER_STATE] == CHARGER_ON) {
  } else if (activations[CHARGER_STATE] == CHARGER_OFF) {

  pollingInterval = useHttpVoltage ? debug_poll[modes[currentState]][MIN] : poll[modes[currentState]][MIN];

unsigned long currentEpoch() {
  unsigned long secs = (millis() - last_millis) / 1000UL;
  return unixTime + secs;

void updateEpoch() {
  last_millis = millis();
  LOG(String("Current ntp time: ") + unixTime);
  lastNtpUpdate = unixTime = ntpUnixTime();
  LOG(String("Updated time: ") + unixTime);

// CREDIT: https://www.oryx-embedded.com/doc/date__time_8h.html

unsigned char computeDayOfWeek(unsigned int y, unsigned char m, unsigned char d)
  unsigned int h;
  unsigned int j;
  unsigned int k;

  //January and February are counted as months 13 and 14 of the previous year
  if (m <= 2)
    m += 12;
    y -= 1;

  //J is the century
  j = y / 100;
  //K the year of the century
  k = y % 100;
  //Compute H using Zeller's congruence
  h = d + (26 * (m + 1) / 10) + k + (k / 4) + (5 * j) + (j / 4);

  //Return the day of the week
  return ((h + 5) % 7) + 1;

void convertUnixTimeToDate(unsigned long t, DateTime *date)
  unsigned long a;
  unsigned long b;
  unsigned long c;
  unsigned long d;
  unsigned long e;
  unsigned long f;

  t -= UTCOFFSET * 3600UL;

  //Negative Unix time values are not supported
  if (t < 1)
    t = 0;

  //Clear milliseconds
  date->milliseconds = 0;

  //Retrieve hours, minutes and seconds
  date->seconds = t % 60;
  t /= 60;
  date->minutes = t % 60;
  t /= 60;
  date->hours = t % 24;
  t /= 24;

  //Convert Unix time to date
  a = (unsigned long) ((4 * t + 102032) / 146097 + 15);
  b = (unsigned long) (t + 2442113 + a - (a / 4));
  c = (20 * b - 2442) / 7305;
  d = b - 365 * c - (c / 4);
  e = d * 1000 / 30601;
  f = d - e * 30 - e * 601 / 1000;

  //January and February are counted as months 13 and 14 of the previous year
  if (e <= 13)
    c -= 4716;
    e -= 1;
    c -= 4715;
    e -= 13;

  //Retrieve year, month and day
  date->year = c;
  date->month = e;
  date->day = f;

  //Calculate day of week
  date->dayOfWeek = computeDayOfWeek(c, e, f);

char * humanDate(DateTime *foo) {
  static char human[22];
  sprintf(human, "%d/%d/%d %02d:%02d %s", foo->month, foo->day, foo->year, getHours(foo) > 12 ? getHours(foo) - 12 : getHours(foo), foo->minutes, getHours(foo) > 12 ? "PM" : "AM");
  return human;

void pollFaster() {
  state check_state = (currentState == STATE_CHARGING && voltage >= ABSORPTION_VOLTAGE) ? STATE_ABSORPTION : currentState;
  int inc = useHttpVoltage ? debug_poll[modes[check_state]][INC] : poll[modes[check_state]][INC];
  int minimum = useHttpVoltage ? debug_poll[modes[check_state]][MIN] : poll[modes[check_state]][MIN];
  if (pollingInterval > minimum) {
    pollingInterval -= inc;
    pollingInterval = max(pollingInterval, minimum);
    LOG(String("Interval lowered to: ") + pollingInterval);
  } else {
    LOG(String("Polling min reached: ") + minimum);

void pollSlower() {
  state check_state = (currentState == STATE_CHARGING && voltage >= ABSORPTION_VOLTAGE) ? STATE_ABSORPTION : currentState;
  int inc = useHttpVoltage ? debug_poll[modes[check_state]][INC] : poll[modes[check_state]][INC];
  int maximum = useHttpVoltage ? debug_poll[modes[check_state]][MAX] : poll[modes[check_state]][MAX];

  if (pollingInterval < maximum) {
    pollingInterval += inc;
    pollingInterval = min(pollingInterval, maximum);
    LOG(String("Interval raised to: ") + pollingInterval);
  } else {
    LOG(String("Polling max reached: ") + maximum);

void logm(String msg) {
   const char logServer[] = ""; // Setup an rsyslog somewhere so you can get log messages.
   String lmsg = String(rsyslogcode);
   static char cmsg[LOGSIZE];
   udp2.begin(1234); // open socket on arbitrary port
   udp2.beginPacket(logServer, 514);
   lmsg += msg;
   lmsg.toCharArray(cmsg, LOGSIZE, 0);

String IpAddress2String(const IPAddress& ipAddress)
  return String(ipAddress[0]) + String(".") + String(ipAddress[1]) + String(".") + String(ipAddress[2]) + String(".") + String(ipAddress[3]);

void setup() { 
  IPAddress ip;
  /* Modify these IPs according to your own static DHCP. The device will self-identify
   * and you'll be able to separate out which log messages belong to which device.
  IPAddress shed = IPAddress(192, 168, 1, 26);         
  IPAddress masterac = IPAddress(192, 168, 1, 150);

  // declare RELAY to be an output:
  pinMode(TransferControl1, OUTPUT);
  pinMode(TransferControl2, OUTPUT);
  pinMode(ChargerControl, OUTPUT);


  // check for the WiFi module:
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("Communication with WiFi module failed!");
    // don't continue
    while (true);

  String fv = WiFi.firmwareVersion();
  if (fv < "1.0.0") {
    Serial.println("Please upgrade the firmware");

  Serial.print("Attempting to connect to WPA SSID: ");
  ip = WiFi.localIP();
  if (ip == masterac) {
    //local5 debug
    rsyslogcode = String("<167>arduino solardivert ");
    sun_start_actual = SUN_START_LEAD;
    sun_stop_actual = SUN_STOP_LEAD;
    idle_full_voltage = IDLE_FULL_VOLTAGE_LEAD;
    empty_voltage = EMPTY_LEAD;
  } else {
    rsyslogcode = String("<175>arduino solardivert ");
    sun_start_actual = SUN_START_LITHIUM;
    sun_stop_actual = SUN_STOP_LITHIUM;
    idle_full_voltage = IDLE_FULL_VOLTAGE_LITHIUM;
    empty_voltage = EMPTY_LITHIUM;
  LOG(String("IP address : ") + IpAddress2String(ip));
  LOG(String("SSID: ") + WiFi.SSID());
  LOG(String("signal strength (RSSI): ") + WiFi.RSSI());
  statusVoltageElapsed = connectionCheckElapsed = readVoltageElapsed = checkVoltageElapsed = unixTime;
  LOG(String("Polling interval: ") + pollingInterval);

enum state evalStart(bool idle, DateTime *now) {
  if (SUN_SHINING(now)) {
    if (idle) {
      // If the sun is shining, we really have no idea what happened
      // The last time the device was reset. If that's the case, then
      // let's just always assume that the last charging session didn't
      // finish so that we get as long of an opportunity to charge as possible.
      return STATE_CHARGING;
  } else {
      if (voltage >= idle_full_voltage) {
        return STATE_DISCHARGING;
      } else {
        return STATE_DARK;

  return STATE_START;

enum state evalDischarging(bool idle, DateTime *now) {
  if (SUN_SHINING(now)) {
    // Don't attempt to over charge the battery if it's already mostly-full.
    if (voltage >= idle_full_voltage) {
      return STATE_SOLAR;
    return STATE_CHARGING;
  if (voltage <= empty_voltage) {
    if (!SUN_SHINING(now)) {
      return STATE_DARK;
    return STATE_CHARGING;


enum state evalDark(bool idle, DateTime *now) {
  if (SUN_SHINING(now)) {
    return STATE_CHARGING;
  } else if (idle && voltage >= 12.22) {
    /* The problem is that:
     *  When discharging, the voltage bounces around a lot.
     *  Within the same 1-2 minute interval, the voltage can easily be
     *  less than .05 difference, even though the battery is still discharging.
     *  This accidentally causes us to use the wrong voltage based on idleness
     *  and make us think that we're idle when we're in fact not idle, which
     *  kicks us back into dark mode. Then what ends up happening is that we
     *  end up switching back and forth from dark to discharging and we're
     *  not using the battery as much. We're going to have to come up with
     *  a special case for this, because the polling interval is not 
     *  working correctly.

  return STATE_DARK;

 * I ended up combining the absorption states
 * and the bulk charging states logically into one state
 * because both states needed to handle the same transitions
 * when the sun is shining and NOT shining. Having them
 * in separate states made it too complex. So, instead I 
 * introduced "reached_maintenance_max" to know if we have
 * entered the absorption phase or not.
enum state evalCharging(bool idle, DateTime *now) {
  // over-voltage protection.
  if (voltage >= MAX_CHARGING_VOLTAGE) {
    reached_maintenance_max = false;
    pollingInterval = poll[modes[currentState]][MIN];
    LOG("Resetting absoprtion state to false.");
  } else if (idle && voltage >= MAINTENANCE_MIN && voltage <= MAINTENANCE_MAX && reached_maintenance_max) { 
    if (SUN_SHINING(now)) {
      return STATE_SOLAR;
    } else {
  } else if (!SUN_SHINING(now)) {
    return STATE_DARK;
  } else if (voltage > MAINTENANCE_MAX) {
    // Stay in a charging state, but make sure we pass this voltage
    // first so we know that the charger has not yet entered the maintenance stage.
    LOG("Reached absoprtion state. Setting to true");
    reached_maintenance_max = true;


enum state evalSolar(bool idle, DateTime *now) {
  if ( ! SUN_SHINING(now)) {

  return STATE_SOLAR;

void reconnect() {
  while(!ScanSSIDs()) {

void WiFiConnect() {
 while(1)  {
   Serial.println("Retrying connection: " + String(WiFi.status()) + " ***");
   if(WiFi.begin(ssid, pass) == WL_CONNECTED) {
   } else {
     Serial.println("Reconnection failed.");

bool ScanSSIDs() {
 int numSsid = WiFi.scanNetworks();
 if(numSsid == -1) 
  return false;
 for(int thisNet = 0; thisNet < numSsid; thisNet++) 
  if(strcmp(WiFi.SSID(thisNet), ssid) == 0) 
    return true; //if one is = to my SSID
 return false;

void loop() {
  unsigned long now = currentEpoch();
  unsigned long diff;

  diff = now - connectionCheckElapsed;
  if (diff >= 10) {
    int StatusWiFi = WiFi.status();
    if((StatusWiFi == WL_CONNECTION_LOST) || (StatusWiFi == WL_DISCONNECTED)) {
      Serial.println("CONNECTION LOST.");
    connectionCheckElapsed = now;

  diff = now - lastNtpUpdate;
  if (diff > 86400) { // every day, ping NTP
  //if (diff > 30) { // every day, ping NTP

  diff = now - readVoltageElapsed;
  if (diff >= 1) {

    readVoltageElapsed = now;

    // only do this once, just to set last voltage for the first time.
    if (full && count == MAX_VALUES) {
      last_voltage = voltage;
      checkVoltageElapsed = now;

  diff = now - statusVoltageElapsed;
  if (diff >= 30) {
  //if (diff >= 5) {
    DateTime foo;
    float voltage_diff = fabs(voltage - last_voltage);
    convertUnixTimeToDate(now, &foo);
    statusVoltageElapsed = now;
    LOG(String("We are: ") + (idle ? "IDLE" : "NOT IDLE"));
    LOG(String("Voltage: ") + voltage + "v, state: " + String(currentStateDescriptions[currentState]));
    LOG(String("Seconds until next check: ") + (pollingInterval - (now - checkVoltageElapsed)));
    LOG(String("Last voltage: ") + last_voltage + "v");
    LOG(String("diff: ") + voltage_diff + "v");

  diff = now - checkVoltageElapsed;
  if (diff >= pollingInterval) {
    if (full) {
      float voltage_diff; 
      state nextState;
      DateTime foo;

      voltage_diff = fabs(voltage - last_voltage);

      convertUnixTimeToDate(now, &foo);

      LOG(String("check voltage elapsed: ") + humanDate(&foo));
      LOG(String("Current voltage: ") + voltage + "v, diff: " + voltage_diff + "v");
      LOG("We are: ");
      if (voltage_diff > MAX_DIFF) {
        LOG("NOT IDLE");
        idle = false;
      } else {
        idle = true;

      switch (currentState) {
        case STATE_START:
          nextState = evalStart(idle, &foo);
        case STATE_CHARGING:
          nextState = evalCharging(idle, &foo);
          nextState = evalDischarging(idle, &foo);
        case STATE_DARK:
          nextState = evalDark(idle, &foo);
        case STATE_SOLAR:
          nextState = evalSolar(idle, &foo);
          nextState = currentState;

      if (nextState != currentState) {
        LOG(String("Changing ") + currentStateDescriptions[currentState]);
        LOG(String("TO => ") + currentStateDescriptions[nextState]);
        currentState = nextState;
      } else {
        LOG(String("NO CHANGE: ") + currentStateDescriptions[currentState]);
      last_voltage = voltage;
    } else {
      LOG("Queue not full yet.");
    checkVoltageElapsed = now;

   * Finally, expose an HTTP server in the device that allows you to access
   * the device from your phone or laptop and monitor what's going on.
   * The rsyslogd support above is way more useful, but this is good
   * to if you just wanna take a quick glance at what's going on.
   * I even included some (crude) attempts below to reset the state
   * machine in case anything goes wrong if you follow along closely.

  // listen for incoming clients
  WiFiClient client = server.available();

  if (client) {
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    boolean httpVoltage = false;
    int lineLocation = 0;
    char line[MAXLINE];

    line[0] = '\0';

    while (client.connected()) {

      if (client.available()) {
        char c = client.read();

        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply

        if (c == '\n' && currentLineIsBlank) {
          DateTime foo;
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          convertUnixTimeToDate(now, &foo);
          client.print("now: ");
          client.print("We are: ");
          if (idle) {
          } else {
            client.println("NOT IDLE");
          client.print("Polling Interval: ");
          client.print("Seconds until next check: ");
          client.println(pollingInterval - (now - checkVoltageElapsed));
          client.print("Last voltage: ");
          client.print("voltage: ");

          if (httpVoltage) {
            int x = 0;
            char hour[10];

            // See if the hour is at the end of the URL
            for (x = 0; x < MAXLINE; x++) {
              if (line[x] == '\0') {
              if (line[x] == '&' && line[x + 1] != '\0') {
                int y, z = 0;
                line[x] = '\0';
                for (y = x + 1; y < MAXLINE; y++) {
                  hour[z++] = line[y];
                  line[y] = '\0';
                hour[z] = '\0';

                client.print("Test HOUR: ");
                httpHours = atoi(hour);
            client.println("Switching to HTTP voltage");
            httpVoltageReading = atof(line);
            useHttpVoltage = true;
            httpVoltage = false;
          } else {
            if (useHttpVoltage) {
              client.println("Disabling HTTP voltage.");
              useHttpVoltage = false;
              httpHours = foo.hours;

          client.print("Current state: ");
          if (!full) {
            client.println("Queue not full yet.");

        } else {
          if (lineLocation < (MAXLINE - 1)) {
            line[lineLocation++] = c;
            line[lineLocation] = '\0';
            if (httpVoltage && c == ' ') {
              line[lineLocation - 1] = '\0';
            if (!httpVoltage && strcmp(line, "GET /?v=") == 0) {
              httpVoltage = true;
              lineLocation = 0;
              line[0] = '\0';
            } else if(strcmp(line, "GET /?reset") == 0) {
              /// This is not working on the Arduino Uno Wifi Rev2
              // Likely that I have a broken RTC, due to: https://github.com/arduino/ArduinoCore-megaavr/issues/27
              for(;;) { 
                // do nothing and wait for the eventual...
              currentState = STATE_START;

        if (c == '\n') {
          // you're starting a new line
          currentLineIsBlank = true;
        else if (c != '\r') {
          // you've gotten a character on the current line
          currentLineIsBlank = false;

    // give the web browser time to receive the data
    // close the connection: