I've mounted 4 nominal 18V 20W "car battery maintaining" solar panels and a 5th of lessor output and am monitoring them with an ESP32-S2Mini running Annex and 5 INA226 current monitor modules--all on a breadboard with small 12V LEDs providing some load. The ESP sends via UDP average voltage, wattage, and amperage every 6 minutes for each of the panels. Another ESP32-S2Mini receives the UDP messages, displays them on an LCD, and logs them with a timestamp in a daily log file. I plan to automate continuous updating of a graph like the one above, but at present I just download the daily log file to yet another ESP32-S2Mini and produce the graph for the day.
I am not confident that I have the INA226s properly calibrated for wattage, but for my present purpose, it doesn't matter, since shading by clouds is clearly shown, and also late afternoon shading by a tree. This should give me what I want to see over the winter.
The INA226 modules have 3 solder blobs which you can variously short out or not, giving in theory 4 I2C addresses, but no matter how they were shorted, I could not get more than three addresses--&h40, &h41, and &h45. I wanted to monitor at least 5 panels anyway, and found that in Annex I could just change the I2C.SETUP pins to access as many INA226 modules as I wanted.
Breadboard in place pic: Here's the[program to read INA226s
Code: [Local Link Removed for Guests]
' ina226x5.bas
' https://www.ti.com/lit/ds/symlink/ina226.pdf?ts=1719076066708 TI data sheet
' https://github.com/macgeorge/STM32-example-codes/blob/master/6%20-%20INA226/F7_INA226.c
'
rConfig=0:rShuntV=1:rBaseV=2:rPower=3:rCurrent=4:rCalib=5:rMask=6:rAlert=7:rManuf=&hFE:rDieID=&hFF
iConfig=0:iShuntV=0:iBaseV=0:iPower=0:iCurrent=0:iCalib=0:iMask=0:iAlert=0:iManuf=0:iDieID=0
i=0:j=0:k=0:l=0:m=0:n=0:reg=0: flagI2C2=0:port=&h40:flagLog=0
rstReason=BAS.RESETREASON ' 0=poweron; 1=HW watchdog; 2=exception; 3=SW watchdog;
' 4=REBOOT; 5=sleep; 6=HW reset
dim rst$(6)="poweron","HW watchdog","exception","SW watchdog","REBOOT","sleep","HW reset"
a$ = "Starting ina226x5.bas "+str$(rstReason)+" "+rst$(rstReason)
a$=a$+" "+date$+" "+time$
wlog a$
FILE.APPEND "/data/startup.log", a$+chr$(13)+chr$(10)
nReads=90 ' 60 is 2 minutes, 720/day; 30=1440/day;90=540/day;120=360/day
dim sumVolts(5)
dim sumWatts(5)
dim sumAmps(5)
dim ia(3) ' input buffer,
dim ib(8) ' register values
dim sReg$(9)="Config","ShuntV","BaseV","Power","Current","Calib"," Mask","Alert","Manuf","DieID"
dim sLoc$(4)="Upper ","Middle"," South "," Lower "," Side"
p$=" "
dim ports(5)=&h40,&h41,&h45,&h40,&h45
dim wifi_APs$(2)="Omnibus_N_EXT","Omnibus_N"
currentWIFI=0 ' or 1
wlog "connected to "+wifi_APs$(currentWIFI)
'wifi_retry_timeout=DATEUNIX(date$)+TIMEUNIX(time$)+10*60 ' 10 minutes from now
'i2c.ReadRegArray port,rConfig,2,ia()
'ib(0)=ia(0)*256+ia(1)
'wlog hex$(ib(0)) ' returns '4127--power-on configuration reg value
'OPTION.WDT 240000 ' milliseconds -- every 4 minutes
udp.begin 39191
a$=" "
for i=0 to 7
a$=a$+sReg$(i)+space$(10-len(sReg$(i)))
next i
wlog a$
wlog p$+" "+sLoc$(0)+p$+sLoc$(1)+p$+sLoc$(2)+p$+sLoc$(3)+p$+sLoc$(4)
lastHr$=""
iCalib = 0.00512 / ( 0.001 * 0.01 )
' wlog "Calib: ";iCalib ' 512 for .010 Ohms, 1mv/LSB
lastSec$=mid$(time$,8,1)
cnt=0 ' sum for 60 2-second periods, then save values
do
if lastSec$<>mid$(time$,8,1) then
lastSec$=mid$(time$,8,1)
lstSec = val(lastSec$) mod 2
flagOK=0 ' do we have at least one non-zero voltage
if lstSec=0 then
I2C.SETUP 33, 35 ' set I2C port on pins 33-sda and 35-scl--esp32-S2Mini
for k=0 to 4
if k=3 then ' switch I2C pins
I2C.SETUP 17, 21 ' set I2C port on pins 17-sda and 21-scl--esp32-S2Mini
pause 100
endif
port=ports(k)
ia(0)=iCalib/256: ia(1)=iCalib-(ia(0)*256)
i2c.WriteRegArray port,rCalib,2,ia()
gosub outputvals
next k
cnt=cnt+1
if cnt>(nReads-1) then
cnt=0
rec$=""
rec2$=""
' save 3-minute sums
for k=0 to 4
fVolts=sumVolts(k)/nReads ' avg of 2min readings every 2 secons
fWatts=sumWatts(k)/nReads*10
fAmps=sumAmps(k)/nReads*10
if fVolts<6 then ' not valid (or dark)
fVolts=0
fWatts=0
fAmps=0
endif
rec$=rec$+str$(fVolts,"%4.1f")+" "+str$(int(fWatts))+" "+str$(int(fAmps))+" "
' rec2$=rec2$+str$(fVolts,"%04.1f")+"V "+str$(int(fWatts),"%5d",1)+" "+str$(int(fAmps),"%5d",1)+" "
rec2$=rec2$+str$(fVolts,"%04.1f")+"V "+str$(int(fWatts),"%05d",1)+" "+str$(int(fAmps),"%05d",1)+" "
' rec2$=rec2$+str$(fVolts,"%04.1f")+"V "+str$(fWatts,"%5f",1)+" "+str$(fAmps,"%5f",1)+" "
sumVolts(k)=0
sumWatts(k)=0
sumAmps(k)=0
if fVolts>5 then flagOK=1
next k
k=instr(rec$," ")
do while k>0
rec$=mid$(rec$,1,k)+mid$(rec$,k+2)
k=instr(rec$," ")
loop
k=instr(rec2$," ")
do while k>0
rec2$=mid$(rec2$,1,k)+mid$(rec2$,k+2)
k=instr(rec2$," ")
loop
if flagOK then
dt$=mid$(date$,7,2)+mid$(date$,4,2)+mid$(date$,1,2)+mid$(time$,1,2)+mid$(time$,4,2)+mid$(time$,7,2)
wlog dt$+" "+rec2$
if lastHr$<>mid$(time$,2,1) then
lastHr$=mid$(time$,2,1)
wlog p$+" "+sLoc$(0)+p$+sLoc$(1)+p$+sLoc$(2)+p$+sLoc$(3)+p$+sLoc$(4)
endif
if WIFI.STATUS<>3 then ' <>3=not ok
while WIFI.STATUS<>3 ' disconnected
if currentWIFI=0 then
currentWIFI=1
else
currentWIFI=0
endif
WIFI.CONNECT wifi_APs$(currentWIFI), “amber1977”
pause 2000
wend
wlog "connected to "+wifi_APs$(currentWIFI)
endif
udp.write "192.168.2.22", 39191, rec$
endif
' OPTION.WDTRESET ' reset watchdog timer
endif
endif
endif
loop
end
outputvals:
a$ = "I2C &h"+hex$(port)
for i=0 to 7
i2c.ReadRegArray port,i,2,ia()
ib(i)=ia(0)*256+ia(1)
select case i
case rBaseV: fBaseV=int(ib(i)*0.00125*100)/100
sumVolts(k)=sumVolts(k)+fBaseV
b$=str$(fBaseV,"%3.1f")+"V" ' to hundredths of a volt
case rPower: fPower=ib(i)
sumWatts(k)=sumWatts(k)+fPower
b$=str$(fPower,"%4.1f")+"mW"
case rCurrent: fCurrent=ib(i)/25
sumAmps(k)=sumAmps(k)+fCurrent
b$=str$(fCurrent,"%6.1f")+"mA"
case else: b$=hex$(ib(i))
end select
a$=a$+space$(10-len(b$))+b$
next i
' wlog str$(cnt)+" "+str$(k)+" "+str$(sumVolts(k))+" "+str$(sumWatts(k))+" "+str$(sumAmps(k))
if flagLog then wlog a$
return
One other option for mounting additional panels is on the East-Southeast-facing roof of my barn. Whenever I can get someone here who can go up on a ladder for me I have another mini-panel prepared which can be screwed on to the roof trim to give me that orientation and height.
The graph should tell me what winter days look like, on average, and how they compare with summer days.
Graphing program:
Code: [Local Link Removed for Guests]
'****************************************************************************************
'* *
'* Example of Chart.js persistant time graph with variable data sets *
'* V1.0.0 16/06/2023 © BeanieBots. *
'* Thanks to cicciocb for the help with html & jarva bits. *
'* For more information on the use of Chart.js, please see https://www.chartjs.org/ *
'* Tested on ESP8266 FW 1.44.2 & ESP32 FW BLE CAN 1.48.22 *
'* *
'****************************************************************************************
RAMspare = 60000 'The amount of RAM left spare after creating the datasets.
'Determines how long data will be stored before deleting.
'The ESP8266 can be as low 32000 but the ESP32 requires at
'least 60000 to be reliable.
NumSets = 4 'The number of EXTRA datasets to be plotted.
Dim Value(NumSets) 'Create an array to hold the new values.
Dim DataSet$(NumSets) 'Create a string array to hold each dataset.
Dim DataSetLabel$(NumSets)="Upper","Middle","South","Lower","Left"
sMoAbbrev$="JanFebMarAprMayJunJulAugSepOctNovDec"
Angle = 0 'Starting value for dummy data
TimeStamp$ = "" 'Declare empty timestamp.
lineNo=0 ' LB: line # of input file to read
flagDone=0 ' LB: end of input file
type$="Watts" ' LB otherwise, Volts
if type$="Watts" then
voltswatts$="Watts"
else
voltswatts$="Volts"
endif
'fn$="/data/240820.txt"
fn$=""
do
if fn$<>"" then
mo=val(mid$(fn$,9,2))
sMo$=mid$(sMoAbbrev$,(mo-1)*3+1,3)
dt$=sMo$+" "+mid$(fn$,11,2)+", 20"+mid$(fn$,7,2)
wlog dt$
Gosub getData ' LB: read file of data
'-----------------------------------------------------------------------------------------
OnHtmlReload ReLoadPage'Re-Load page for new connections.
'-----------------------------------------------------------------------------------------
Gosub LoadPage 'Load initial page
'Timer0 1000, NextData 'Do not set < 2000 (LB: was 3000)
wlog "Graph Completed for "+voltswatts$+" on "+dt$
fn$=""
endif
loop
wait
'-----------------------------------------------------------------------------------------
NextData:
if not flagDone then
Gosub GetTimeStamp 'Each piece of data requires a timestamp in string format
Gosub GetValue 'Generate some example data.
Gosub UpdateChart 'Send data to page and update
while ramfree < RAMspare 'When low on RAM start removing old data from the left.
For Set = 0 to NumSets 'Remove data from the left
DataSet$(Set) = right$(DataSet$(Set),(len(DataSet$(Set))-instr(DataSet$(Set),"}")-1))
Next Set
'An alternative (especially if datasets have different frame rates) would be to set
'a maximun string length (possibly for each set individualy) and then trim accordingly.
wend
endif
Return
'-----------------------------------------------------------------------------------------
GetTimeStamp: '-1 hour (60*60*1000) for GB time.
TimeStamp = (dateUNIX(date$)+timeunix(time$))*1000-(60*60*1000)
Timestamp$ = str$(TimeStamp,"%0.0f")
Return
'-----------------------------------------------------------------------------------------
UpdateChart:
For Set = 0 to NumSets 'Add the new data to the existing string for each dataset.
DataSet$(Set) = DataSet$(Set) + "{x:"+TimeStamp$+",y:"+str$(Value(Set))+"},"
jscall "mychart.data.datasets[" + str$(Set )+ "].data = [" + DataSet$(Set) + "];"
Pause 100 'wait for data to load
Next Set
jscall "mychart.update();" 'now update the chart.
Return
'-----------------------------------------------------------------------------------------
GetData:
lineNo=1
line$=FILE.READ$(fn$,lineNo) ' 240805B
wlog lineNo,mid$(line$,1,30)
tmStamp$=mid$(line$,1,12)
uDate$=mid$(tmStamp$,4,2)+"/"+mid$(tmStamp$,3,2)+"/"+mid$(tmStamp$,1,2)
uTime$=mid$(tmStamp$,7,2)+":"+mid$(tmStamp$,9,2)+"/"+mid$(tmStamp$,11,2)
TimeStamp = (dateUNIX(uDate$)+timeunix(uTime$)-3600)*1000 ' 3600 adjust for DST
TimeStamp$ = str$(TimeStamp,"%0.0f")
For Set = 0 to NumSets 'Add the new data
DataSet$(Set) = DataSet$(Set) + "{x:"+TimeStamp$+",y:"+str$(Value(Set))+"},"
Next Set
do while line$<>""
tmStamp$=mid$(line$,1,12)
uDate$=mid$(tmStamp$,4,2)+"/"+mid$(tmStamp$,3,2)+"/"+mid$(tmStamp$,1,2)
uTime$=mid$(tmStamp$,7,2)+":"+mid$(tmStamp$,9,2)+"/"+mid$(tmStamp$,11,2)
TimeStamp = (dateUNIX(uDate$)+timeunix(uTime$))*1000
TimeStamp$ = str$(TimeStamp,"%0.0f")
fst=14 ' beginning of data
loc=instr(Fst,line$," ") ' point to end of first volts value
if type$="Watts" then
fst=loc+1 ' move to first Watts value
loc=instr(Fst,line$," ") ' move past watts
endif
For Set = 0 to NumSets 'Add the new data
Value(Set) = val(mid$(line$,fst,loc-fst))
if type$="Watts" then
Value(Set) = Value(Set)/1000 ' convert milliwatts to watts
endif
fst=loc+1
loc=instr(Fst,line$," ") ' move past watts (or amps)
fst=loc+1
loc=instr(Fst,line$," ") ' move past amps (or volts)
fst=loc+1 ' point to next target: volts or milliwats
loc=instr(Fst,line$," ") ' end of next target
DataSet$(Set) = DataSet$(Set) + "{x:"+TimeStamp$+",y:"+str$(Value(Set))+"},"
Next Set
if lineNo mod 50 = 0 then wlog lineNo,mid$(line$,1,30)
lineNo=lineNo+1
line$=FILE.READ$(fn$,lineNo)
' if lineNo=51 then line$=""
loop
For Set = 0 to NumSets 'Add the new data
DataSet$(Set) = DataSet$(Set) + "{x:"+TimeStamp$+",y:"+str$(0.0)+"},"
Next Set
wlog "Finished reading File; "+str$(lineNo)+" records"
file.write "/data/chartdata.txt",DataSet$(0)
For Set = 1 to NumSets 'Add the new data
file.append "/data/chartdata.txt",DataSet$(Set)
Next Set
jscall "mychart.update();" 'now update the chart.
return
'-----------------------------------------------------------------------------------------
LoadPage:
cls
jsexternal "https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js"
pause 500 'wait a little bit for the library to load
jsexternal "https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"
'Required for 'time' scale plots.
pause 100 'wait a little bit for the library to load
cls
A$=""
A$ = |<h><center> Solar Panel Output: |+dt$+| -- |+voltswatts$+| </center></h>|
A$ = A$ + |<div>|
A$ = A$ + | <canvas id="myChart"></canvas>|
A$ = A$ + |<style> body {background-color: #cce;}</style>|
A$ = A$ + |</div>|
html a$
'Generate the chart
A$ = ||
A$ = A$ + |ctx = document.getElementById('myChart');|
A$ = A$ + ||
A$ = A$ + | mychart = new Chart(ctx, {|
A$ = A$ + | type: 'line',|
A$ = A$ + | data: {|
'Add the first dataset
A$ = A$ + | datasets: [{|
A$ = A$ + | label: 'Upper',|
A$ = A$ + | data: [],|
'Add subsequent datasets (if any)
For Set = 1 to NumSets
Title$ = DataSetLabel$(Set)
A$ = A$ + | },{|
A$ = A$ + | label: '|+Title$+|',|
A$ = A$ + | data: [],|
Next Set
'Close off the datasets, set scales and add any options.
A$ = A$ + | },]|
A$ = A$ + | },|
A$ = A$ + | options: {|
A$ = A$ + | scales: {|
A$ = A$ + | x: {|
A$ = A$ + | type: 'time',|
A$ = A$ + | time: {unit: 'minute'},|
A$ = A$ + | }|
A$ = A$ + | }|
A$ = A$ + | }|
A$ = A$ + | });|
jscript A$
A$ = ""
Pause 100
Return
'-----------------------------------------------------------------------------------------
ReLoadPage:
Gosub LoadPage
If TimeStamp$ <> "" then 'Don't update an empty graph.
Gosub UpdateChart
endif
Return
'-----------------------------------------------------------------------------------------
End