├── .gitignore ├── imgs ├── solar1.jpg ├── solar2.jpg ├── solar3.jpg ├── solar4.jpg └── solar5.jpg ├── requirements.txt ├── README.md └── report.py /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | -------------------------------------------------------------------------------- /imgs/solar1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamikm/solar-report/HEAD/imgs/solar1.jpg -------------------------------------------------------------------------------- /imgs/solar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamikm/solar-report/HEAD/imgs/solar2.jpg -------------------------------------------------------------------------------- /imgs/solar3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamikm/solar-report/HEAD/imgs/solar3.jpg -------------------------------------------------------------------------------- /imgs/solar4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamikm/solar-report/HEAD/imgs/solar4.jpg -------------------------------------------------------------------------------- /imgs/solar5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamikm/solar-report/HEAD/imgs/solar5.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.4.5.1 2 | chardet==3.0.4 3 | idna==2.9 4 | requests==2.23.0 5 | urllib3==1.25.9 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solar Report 2 | Gets solar energy production, household consumption, and net household consumption from Enphase Enlighten. 3 | 4 | ## Usage 5 | ``` 6 | $ virtualenv env 7 | $ . env/bin/activate 8 | $ pip install -r requirements.txt 9 | $ python report.py --start 3/4/20 --end 4/2/20 10 | 11 | off peak consumption (< 08:00) 383.2 kwh 12 | super off consumption (< 16:00) 221.1 kwh 13 | peak consumption (< 21:00) 218.8 kwh 14 | 15 | off peak production (< 08:00) 9.3 kwh 16 | super off production (< 16:00) 781.5 kwh 17 | peak production (< 21:00) 97.2 kwh 18 | 19 | off peak net (< 08:00) 373.9 kwh 20 | super off net (< 16:00) -560.3 kwh 21 | peak net (< 21:00) 121.5 kwh 22 | ``` 23 | 24 | Follow the [Enphase API Quickstart](https://developer.enphase.com/docs/quickstart.html) and replace `ENPHASE_APP_KEY`, `ENPHASE_USER_ID`, and `ENPHASE_SYSTEM_ID` in the script to hook up your own system. 25 | 26 | This script uses the SoCal Edison time of use 4-9 rate plan (TOU-D-4-9). Change `OFF_PEAK_END_HR`, `SUPER_OFF_PEAK_END_HR`, `PEAK_END_HR` for different rate plans. 27 | 28 | ## Pictures from my first "physical" side project 29 | I installed my own 6.8 kW solar panel system in 2019 to take advantage of the 30% federal tax credit before it dropped. Not including four weekends of labor, it cost $11,200 after the credit and will pay itself off in about five years. 30 | 31 | I modeled my permit application after a friend's, which made the whole permitting ordeal considerably more tractable than it would otherwise have been. Even so, it took about sixteen hours to design my system and write my permit, which was — somehow? — stamped on my first try. Inspection was also unexpectedly straightforward: after glacing at my electrical panel, the inspector signed off without even going on my roof! He said, "all those guys do a good job anyway." I held my tongue. 32 | 33 | It took another several hours to finish my permission-to-operate application with SoCal Edison (SCE) and opt into net metering and time-of-use billing, which is required for net metering clients. I got an unexpectedly high first bill; my energy is _delivered_ by SCE but _generated_ by Clean Power Alliance (CPA). CPA has a surplus of solar energy, so it charges a negative super off peak rate. After switching over to SCE for generation, my already-improved electrical bill improved by nearly $50 per month. 34 | 35 | ![alt text](/imgs/solar1.jpg) 36 | I built a trellis/ramp to make it easier to get twenty-one 50 lb panels on my roof. 37 | 38 | ![alt text](/imgs/solar2.jpg) 39 | It took an entire day for me to find rafters and plan my roof penetrations with sidewalk chalk. 40 | 41 | ![alt text](/imgs/solar3.jpg) 42 | Those boards on the rails on Enphase Microinverters. 43 | 44 | ![alt text](/imgs/solar4.jpg) 45 | Here's the roof part of the finished product! 46 | 47 | ![alt text](/imgs/solar5.jpg) 48 | Here's the ground part! The Enphase Envoy is on the left. It combines the two home runs from the roof, uses current transformers to monitor production and consumption, and reports those numbers at 15 minute intervals to the cloud. 49 | -------------------------------------------------------------------------------- /report.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from datetime import datetime 3 | import requests 4 | import time 5 | from operator import sub 6 | import os 7 | 8 | ENPHASE_APP_KEY = os.environ['ENPHASE_APP_KEY'] 9 | ENPHASE_USER_ID = os.environ['ENPHASE_USER_ID'] 10 | ENPHASE_SYSTEM_ID = os.environ['ENPHASE_SYSTEM_ID'] 11 | 12 | AUTH_PARAMS = { 13 | 'key': ENPHASE_APP_KEY, 14 | 'user_id': ENPHASE_USER_ID 15 | } 16 | TIME_INTERVAL_PARAMS = lambda start, end: { 17 | 'start_at': start, 18 | 'end_at': end 19 | } 20 | ENPHASE_BASE_URL = 'https://api.enphaseenergy.com/api/v2' 21 | STATS = lambda start, end, isForProduction: '{base}/systems/{sysId}/{endpoint}?{params}'.format( 22 | base=ENPHASE_BASE_URL, 23 | sysId=ENPHASE_SYSTEM_ID, 24 | endpoint='rgm_stats' if isForProduction else 'consumption_stats', 25 | params=getUrlParams(TIME_INTERVAL_PARAMS(start, end), AUTH_PARAMS) 26 | ) 27 | 28 | OFF_PEAK_END_HR = 8 29 | SUPER_OFF_PEAK_END_HR = 16 30 | PEAK_END_HR = 21 31 | WATTS_TO_KWH = .001 32 | DECIMAL_PLACES = 1 33 | FIRST_COL_WIDTH = 40 34 | 35 | def getUrlParams(*args): 36 | '''Return a string consisting of url params of all k v pairs in the dicts in args''' 37 | mergedDict = {} 38 | for arg in args: 39 | mergedDict.update(arg) 40 | return '&'.join(['{}={}'.format(k, v) for k,v in mergedDict.items()]) 41 | 42 | def date(dateString): 43 | '''Convert date from mm/dd/yy format to a Unix timestamp''' 44 | return datetime.strptime(dateString, '%m/%d/%y').timestamp() 45 | 46 | def printStats(cpn, offPeakKwh, superOffPeakKwh, peakKwh): 47 | '''Print stats for consumption, production, or net (cpn)''' 48 | print ('{:40}{} kwh'.format( 49 | 'off peak {} (< {:02}:00)'.format(cpn, OFF_PEAK_END_HR), 50 | round(offPeakKwh, DECIMAL_PLACES) 51 | )) 52 | print ('{:40}{} kwh'.format( 53 | 'super off {} (< {:02}:00)'.format(cpn, SUPER_OFF_PEAK_END_HR), 54 | round(superOffPeakKwh, DECIMAL_PLACES) 55 | )) 56 | print ('{:40}{} kwh'.format( 57 | 'peak {} (< {:02}:00)'.format(cpn, PEAK_END_HR), 58 | round(peakKwh, DECIMAL_PLACES) 59 | )) 60 | print () 61 | 62 | def getStats(start, end, isForProduction, printThem=True): 63 | '''Call Enphase API to get consumption or production stats between given timestamps''' 64 | r = requests.get(url=STATS(start, end, isForProduction)) 65 | intervals = r.json()['intervals'] 66 | 67 | offPeakKwh = 0 68 | superOffPeakKwh = 0 69 | peakKwh = 0 70 | for interval in intervals: 71 | intervalEndTimestamp = interval['end_at'] - 1 72 | hr = int(datetime.fromtimestamp(intervalEndTimestamp).strftime('%H')) 73 | kwh = interval['wh_del' if isForProduction else 'enwh'] * WATTS_TO_KWH 74 | 75 | if hr < OFF_PEAK_END_HR: 76 | offPeakKwh += kwh 77 | elif hr < SUPER_OFF_PEAK_END_HR: 78 | superOffPeakKwh += kwh 79 | elif hr < PEAK_END_HR: 80 | peakKwh += kwh 81 | else: # back to off peak 82 | offPeakKwh += kwh 83 | 84 | state = 'production' if isForProduction else 'consumption' 85 | printStats(state, offPeakKwh, superOffPeakKwh, peakKwh) 86 | 87 | return offPeakKwh, superOffPeakKwh, peakKwh 88 | 89 | if __name__ == '__main__': 90 | # get start and end timestamps from console args 91 | parser = argparse.ArgumentParser( 92 | description='Get consumption report for the given time interval' 93 | ) 94 | parser.add_argument('--start', required=True, help='Start date in mm/dd/yy format', type=date) 95 | parser.add_argument('--end', required=True, help='End date in mm/dd/yy format', type=date) 96 | args = parser.parse_args() 97 | start, end = args.start, args.end 98 | 99 | # get consumption, production, and net kwh in each time-of-use period. print them 100 | print () 101 | consumption = getStats(start, end, isForProduction=False) 102 | production = getStats(start, end, isForProduction=True) 103 | net = map(sub, consumption, production) 104 | printStats('net', *net) 105 | --------------------------------------------------------------------------------