def report_yesterday(data): # Report on yesterdays mileage/efficiency t = datetime.date.today() today_ts = t.strftime("%Y%m%d") t = t + datetime.timedelta(days=-1) yesterday_ts = t.strftime("%Y%m%d") if today_ts not in data["daily_state_am"] or yesterday_ts not in data[ "daily_state_am"]: logT.debug("Skipping yesterday tweet due to missing items") m = None pic = None else: miles_driven = data["daily_state_am"][today_ts]["odometer"] - data[ "daily_state_am"][yesterday_ts]["odometer"] kw_used = data["daily_state_am"][today_ts]["charge_energy_added"] if miles_driven > 200: m = "Yesterday I drove my #Tesla %s miles on a road trip! " \ "@Teslamotors #bot" % ("{:,}".format(int(miles_driven))) elif miles_driven == 0: mileage = data["daily_state_am"][today_ts]["odometer"] today_ym = datetime.date.today() start_ym = datetime.date(2014, 4, 21) ownership_months = int((today_ym - start_ym).days / 30) m = "Yesterday my #Tesla had a day off. Current mileage is %s miles after %d months " \ "@Teslamotors #bot" % ("{:,}".format(int(mileage)), ownership_months) elif data["day_charges"] == 0 or data["day_charges"] > 1: # Need to skip efficiency stuff here if car didnt charge last night or we charged more than once # TODO: Could save prior efficiency from last charge and use that day = yesterday_ts time_value = time.mktime( time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(logT, time_value) m = "Yesterday I drove my #Tesla %s miles. Avg temp %.0fF. " \ "@Teslamotors #bot" \ % ("{:,}".format(int(miles_driven)), w["avg_temp"]) else: # Drove a distance and charged exactly once since last report, we have enough data # to report efficiency. day = yesterday_ts time_value = time.mktime( time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(logT, time_value) efficiency = kw_used * 1000 / miles_driven # If efficiency isnt a reasonable number then don't report it. # Example, drive somewhere and don't charge -- efficiency is zero. # Or drive somewhere, charge at SC, then do normal charge - efficiency will look too high. if kw_used > 0 and efficiency > 200 and efficiency < 700: m = "Yesterday I drove my #Tesla %s miles using %.1f kWh with an effic. of %d Wh/mi. Avg temp %.0fF. " \ "@Teslamotors #bot" \ % ("{:,}".format(int(miles_driven)), kw_used, efficiency, w["avg_temp"]) else: m = "Yesterday I drove my #Tesla %s miles. Avg temp %.0fF. " \ "@Teslamotors #bot" % ("{:,}".format(int(miles_driven)), w["avg_temp"]) pic = os.path.abspath(random.choice(get_pics())) return m, pic
def analyze_weather(data): """ SolarCity has had outages where they cant provide the cloud cover/weather information. This compares SolarCity reported weather data with other weather data. """ for day in sorted(data['data']): d = data['data'][day] if "weather_api" not in d: time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] if 'cloud' in d: ss_cloud = d['cloud'] else: ss_cloud = 0 if 'daylight' in d: ss_daylight = d['daylight'] else: ss_daylight = 0 print "%s Cloud: %d%% Daylight: %.1f (API Cloud: %d%%, Daylight: %.1f)" % (day, ss_cloud, ss_daylight, cloud_cover, daylight_hours) else: if 'cloud' in d: cloud_cover = d['cloud'] else: cloud_cover = 0 if 'daylight' in d: daylight_hours = d['daylight'] else: daylight_hours = 0 print "%s API Cloud: %d%% API Daylight: %.1f" % (day, cloud_cover, daylight_hours)
def get_solarguard_day_data(day=None): append = '' if day: append = '&ChartDate=%d_%d_%d' % (day.month, day.day, day.year) r = requests.get(SOLARGUARD_URL + append) soup = BeautifulSoup(r.text, features="html.parser") daylight_hours = 0 cloud_cover = 0 production = float(soup.find(id='ctl00_cphMain_lbProdTotalByChart').text) if not day and production == 0 and cloud_cover == 0 and daylight_hours == 0: raise Exception( "Problem getting current production level: %.1f, %d, %.1f" % (production, cloud_cover, daylight_hours)) if daylight_hours == 0: """ SolarCity has had outages where they cant provide the cloud cover/weather information. If the weather data appears empty here, we'll go get it from another source """ if day: ts = datetime.datetime.combine( day, datetime.datetime.max.time()).strftime("%s") else: ts = time.time() w = get_daytime_weather_data(log, ts) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] return daylight_hours, cloud_cover, production
def get_day_data(day=None): append = '' if day: append = '?date=%d-%02d-%02d' % (day.year, day.month, day.day) chrome_options = Options() chrome_options.add_argument("--headless") driver = webdriver.Chrome(options=chrome_options) driver.get(SOLARCITY_URL) time.sleep(10) driver.get(SOLARCITY_URL + append) time.sleep(10) data = driver.find_element(By.CSS_SELECTOR, "div.consumption-production-panel").text if data and len(data.split()) > 0: production = float(data.split()[0]) else: production = 0 if day: ts = datetime.datetime.combine( day, datetime.datetime.max.time()).strftime("%s") else: ts = time.time() w = get_daytime_weather_data(log, ts) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] # If we get here everything worked, shut down the browser driver.quit() if os.path.exists("geckodriver.log"): os.remove("geckodriver.log") return daylight_hours, cloud_cover, production
def report_yesterday(data): # Report on yesterdays mileage/efficiency t = datetime.date.today() today_ts = t.strftime("%Y%m%d") t = t + datetime.timedelta(days=-1) yesterday_ts = t.strftime("%Y%m%d") if today_ts not in data["daily_state_am"] or yesterday_ts not in data["daily_state_am"]: logT.debug("Skipping yesterday tweet due to missing items") else: miles_driven = data["daily_state_am"][today_ts]["odometer"] - data["daily_state_am"][yesterday_ts][ "odometer"] kw_used = data["daily_state_am"][today_ts]["charge_energy_added"] if miles_driven > 200: m = "Yesterday I drove my #Tesla %s miles on a road trip! " \ "@Teslamotors #bot" % ("{:,}".format(int(miles_driven))) elif miles_driven == 0: mileage = data["daily_state_am"][today_ts]["odometer"] today_ym = datetime.date.today() start_ym = datetime.date(2014, 4, 21) ownership_months = int((today_ym - start_ym).days / 30) m = "Yesterday my #Tesla had a day off. Current mileage is %s miles after %d months " \ "@Teslamotors #bot" % ("{:,}".format(int(mileage)), ownership_months) elif False: # not is_plugged_in(c, CAR_NAME): # Need to skip efficiency stuff here if car didnt charge last night day = yesterday_ts time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(logT, time_value) m = "Yesterday I drove my #Tesla %s miles. Avg temp %.1fF. " \ "@Teslamotors #bot" \ % ("{:,}".format(int(miles_driven)), w["avg_temp"]) else: day = yesterday_ts time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(logT, time_value) m = "Yesterday I drove my #Tesla %s miles using %.1f kWh with an effic. of %d Wh/mi. Avg temp %.1fF. " \ "@Teslamotors #bot" \ % ("{:,}".format(int(miles_driven)), kw_used, kw_used * 1000 / miles_driven, w["avg_temp"]) pic = os.path.abspath(random.choice(get_pics())) return m, pic
def upload_to_pvoutput(data, day): # Get pvoutput.org login information from environment if 'PVOUTPUT_ID' not in os.environ: raise Exception("PVOUTPUT_ID missing") else: pvoutput_id = os.environ['PVOUTPUT_ID'] if 'PVOUTPUT_KEY' not in os.environ: raise Exception("PVOUTPUT_KEY missing") else: pvoutput_key = os.environ['PVOUTPUT_KEY'] log.debug("Report weather info to pvoutput.org for %s", day) time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) short_description = "Not Sure" if "partly cloudy" in w["description"].lower(): short_description = "Partly Cloudy" if "mostly cloudy" in w["description"].lower(): short_description = "Mostly Cloudy" elif "snow" in w["description"].lower(): short_description = "Snow" elif "rain" in w["description"].lower(): short_description = "Showers" elif "clear" in w["description"].lower(): short_description = "Fine" pvdata = {} pvdata["d"] = day pvdata["g"] = data["data"][day]["production"] * 1000 pvdata["cd"] = short_description pvdata["tm"] = "%.1f" % ((w["low_temp"] - 32) * 5.0 / 9.0) pvdata["tx"] = "%.1f" % ((w["high_temp"] - 32) * 5.0 / 9.0) pvdata["cm"] = "Daylight hours: %.1f, Cloud cover: %d%%" % ( data["data"][day]["daylight"], data["data"][day]["cloud"]) data = urllib.parse.urlencode(pvdata) headers = {} headers["X-Pvoutput-Apikey"] = pvoutput_key headers["X-Pvoutput-SystemId"] = pvoutput_id req = urllib.request.Request( "http://pvoutput.org/service/r2/addoutput.jsp", data.encode('utf-8'), headers) response = urllib.request.urlopen(req) output = response.read() log.debug(" Upload response: %s", output)
def upload_to_pvoutput(data, day): # Get pvoutput.org login information from environment if 'PVOUTPUT_ID' not in os.environ: raise Exception("PVOUTPUT_ID missing") else: pvoutput_id = os.environ['PVOUTPUT_ID'] if 'PVOUTPUT_KEY' not in os.environ: raise Exception("PVOUTPUT_KEY missing") else: pvoutput_key = os.environ['PVOUTPUT_KEY'] log.debug("Report weather info to pvoutput.org for %s", day) time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) short_description = "Not Sure" if "partly cloudy" in w["description"].lower(): short_description = "Partly Cloudy" if "mostly cloudy" in w["description"].lower(): short_description = "Mostly Cloudy" elif "snow" in w["description"].lower(): short_description = "Snow" elif "rain" in w["description"].lower(): short_description = "Showers" elif "clear" in w["description"].lower(): short_description = "Fine" pvdata = {} pvdata["d"] = day pvdata["g"] = data["data"][day]["production"] * 1000 pvdata["cd"] = short_description pvdata["tm"] = "%.1f" % ((w["low_temp"] - 32) * 5.0 / 9.0) pvdata["tx"] = "%.1f" % ((w["high_temp"] - 32) * 5.0 / 9.0) pvdata["cm"] = "Daylight hours: %.1f, Cloud cover: %d%%" % (data["data"][day]["daylight"], data["data"][day]["cloud"]) data = urllib.urlencode(pvdata) headers = {} headers["X-Pvoutput-Apikey"] = pvoutput_key headers["X-Pvoutput-SystemId"] = pvoutput_id req = urllib2.Request("http://pvoutput.org/service/r2/addoutput.jsp", data, headers) response = urllib2.urlopen(req) output = response.read() log.debug(" Upload response: %s", output)
def main(): parser = argparse.ArgumentParser(description='Show sunday weather start of current (or passed) year to today') parser.add_argument('--year', help='Starting year (YYYY)', required=False, default=datetime.datetime.now().year, type=int) args = parser.parse_args() t = datetime.datetime.strptime("%04d01012100" % args.year, "%Y%m%d%H%M") current_year = t.year print "date,avg temp,low temp,cloud cover,precip type,precip probability" while t.year == current_year and t < datetime.datetime.now(): ts = "%04d%02d%02d" % (t.year, t.month, t.day) if t.weekday() == 6: w = get_daytime_weather_data(logging, time.mktime(t.timetuple())) print "%s,%.1f,%.1f,%d%%,%s,%d%%" % (ts, w["avg_temp"], w["low_temp"], w["cloud_cover"], w["precip_type"], w["precip_probability"]) t = t + datetime.timedelta(days=1)
def main(): parser = argparse.ArgumentParser( description= 'Show sunday weather start of current (or passed) year to today') parser.add_argument('--year', help='Starting year (YYYY)', required=False, default=datetime.datetime.now().year, type=int) args = parser.parse_args() t = datetime.datetime.strptime("%04d01012100" % args.year, "%Y%m%d%H%M") current_year = t.year print("date,avg temp,low temp,cloud cover,precip type,precip probability") while t.year == current_year and t < datetime.datetime.now(): ts = "%04d%02d%02d" % (t.year, t.month, t.day) if t.weekday() == 6: w = get_daytime_weather_data(logging, time.mktime(t.timetuple())) print("%s,%.1f,%.1f,%d%%,%s,%d%%" % (ts, w["avg_temp"], w["low_temp"], w["cloud_cover"], w["precip_type"], w["precip_probability"])) t = t + datetime.timedelta(days=1)
def analyze_weather(data): """ SolarCity has had outages where they cant provide the cloud cover/weather information. This compares SolarCity reported weather data with other weather data. """ for day in sorted(data['data']): d = data['data'][day] if "weather_api" not in d: time_value = time.mktime( time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] if 'cloud' in d: ss_cloud = d['cloud'] else: ss_cloud = 0 if 'daylight' in d: ss_daylight = d['daylight'] else: ss_daylight = 0 print( "%s Cloud: %d%% Daylight: %.1f (API Cloud: %d%%, Daylight: %.1f)" % (day, ss_cloud, ss_daylight, cloud_cover, daylight_hours)) else: if 'cloud' in d: cloud_cover = d['cloud'] else: cloud_cover = 0 if 'daylight' in d: daylight_hours = d['daylight'] else: daylight_hours = 0 print("%s API Cloud: %d%% API Daylight: %.1f" % (day, cloud_cover, daylight_hours))
def main(): parser = argparse.ArgumentParser(description='SolarCity Reporting') parser.add_argument('--no_email', help='Dont send emails', required=False, action='store_true') parser.add_argument('--force', help='Force update', required=False, action='store_true') parser.add_argument('--no_tweet', help='Dont post tweets', required=False, action='store_true') parser.add_argument('--report', help='Generate report', required=False, action='store_true') parser.add_argument('--blog', help='Generate html report page', required=False, action='store_true') parser.add_argument('--daily', help='Report on daily generation', required=False, action='store_true') parser.add_argument('--monthly', help='Report on monthly generation', required=False, action='store_true') parser.add_argument('--yearly', help='Report on yearly generation', required=False, action='store_true') parser.add_argument('--weather', help='Report weather for given date (YYYYMMDD)', required=False, type=str) parser.add_argument('--pvoutput', help="Send data for date (YYYYMMDD) to PVOutput.org", required=False, type=str) args = parser.parse_args() # Make sure we only run one instance at a time fp = open('/tmp/solarcity.lock', 'w') try: fcntl.flock(fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except: log.debug( "Sorry, someone else is running this tool right now. Please try again later" ) return -1 log.debug("--- solarcity.py start ---") data = load_data() data_changed = False data = load_historical_data(data) production_max, production_min, total_generation = analyze_data(data) if args.monthly: # First check if its last day of the month log.debug("Check for monthly update") now = datetime.datetime.now() dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of month.") current_month = time.strftime("%Y%m") if 'lastmonthlytweet' not in data['config'] or data['config'][ 'lastmonthlytweet'] != current_month: tweet_month(data) data['config']['lastmonthlytweet'] = current_month data_changed = True else: log.debug(" Not last day of month, skipping. ") if args.yearly: # First check if its last day of the year log.debug("Check for annual update") now = datetime.datetime.now() if now.month == 12: dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of year.") current_month = time.strftime("%Y%m") if 'lastyearlytweet' not in data['config'] or data['config'][ 'lastyearlytweet'] != current_month: tweet_year(data) data['config']['lastyearlytweet'] = current_month data_changed = True else: log.debug(" Not last day of year, skipping. ") if args.weather is not None: log.debug("Check weather data") if int(args.weather) == 0: time_value = int(time.time()) else: time_value = time.mktime( time.strptime("%s2100" % args.weather, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) print("Weather as of %s:" % datetime.datetime.fromtimestamp(time_value)) print(" Average temperature: %.1fF" % w["avg_temp"]) print(" Low temperature: %.1fF" % w["low_temp"]) print(" Cloud Cover: %d%%" % w["cloud_cover"]) print(" Daylight hours: %.1f" % w["daylight"]) print(" Description: %s" % w["description"]) print(" Precipitation type: %s" % w["precip_type"]) print(" Precipitation Chance: %d%%" % w["precip_probability"]) # analyze_weather(data) if args.daily: log.debug("Check for daily update") current_day = time.strftime("%Y%m%d") if data['config'][ 'lastdailytweet'] != current_day or DEBUG_MODE or args.force: daylight_hours, cloud_cover, production = get_current_day_data() data['data'][current_day] = { 'daylight': daylight_hours, 'cloud': cloud_cover, 'production': production } special = None if production_max is None or production > data['data'][ production_max]['production']: special = "high" elif production_min is None or production < data['data'][ production_min]['production']: special = "low" if not args.no_tweet: tweet_production(daylight_hours, cloud_cover, production, special) data['config']['lastdailytweet'] = current_day data_changed = True if args.pvoutput is not None: # Now upload to pvoutput upload_to_pvoutput(data, current_day) elif args.pvoutput is not None: if int(args.pvoutput) == 0: print("Uploading historical data to pvoutput.org") for d in data["data"]: print(" Processing date %s" % d) try: upload_to_pvoutput(data, d) except: print(" problem with date %s" % d) print(" Sleeping") # You'll need longer sleeps if you didnt donate time.sleep(15) else: upload_to_pvoutput(data, args.pvoutput) if args.report: # Send/Run weekly solarcity summary report solarcity_report(data, args.no_email, args.no_tweet) if args.blog: # Export all entries found for posting to static page on blog: teslaliving.net/solarcity log.debug("Reporting on SolarCity generation for blog") print( '<a href="http://share.solarcity.com/teslaliving">@SolarCity</a> Solar Installation' ) print('<h3>System Results</h3>') print("<b>%s total power generated via @SolarCity as of %s</b>" % (show_with_units(total_generation), time.strftime("%Y%m%d"))) print("%s day max on %s" % (show_with_units( data['data'][production_max]['production']), production_max)) print("%s day min on %s" % (show_with_units( data['data'][production_min]['production']), production_min)) print("<b>%s daily average production</b>" % (show_with_units(total_generation / len(data['data'])))) print('<h3>System Details</h3>') print("System size is 69 panels at 255W each = %.1fkW" % (69 * .255)) r = relativedelta(datetime.datetime.now(), datetime.datetime.strptime("2015-02-23", '%Y-%m-%d')) elapsed_months = r.years * 12 + r.months print("System was turned on February 23, 2015 (%d months ago)" % elapsed_months) print("Panel info: ") print("* Size: 1638 x 982 x 40mm (64.5 x 38.7 x 1.57in)") print( "* Vendor: <a href='http://www.canadiansolar.com/solar-panels/cs6p-p.html'>CanadianSolar CS6P-P</a>" ) print("Inverter info: ") print( "* <a href='http://www.solaredge.com/sites/default/files/se-single-phase-us-inverter-datasheet.pdf'>SolarEdge SE6000A</a>" ) print(" ") print( 'Sign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' ) print( '<h3>Chart via <a href="http://pvoutput.org/list.jsp?id=48753&sid=44393">PVOutput</a></h3>[hoops name="pvoutput"]' ) print('<h3>Daily Log:</h3>') print("%s%s%s %s %s" % ("Date", " " * 11, "Production", "Daylight", "Cloud Cover")) d = data['data'] for e in sorted(d, reverse=True): production = d[e]['production'] if 'daylight' in d[e] and d[e]['daylight'] > 0: daylight = d[e]['daylight'] if 'cloud' in d[e]: cloud = d[e]['cloud'] else: cloud = 0 else: w = get_daytime_weather_data( log, time.mktime(time.strptime("%s2100" % e, "%Y%m%d%H%M"))) d[e]['daylight'] = w['daylight'] daylight = w['daylight'] d[e]['weather_api'] = True d[e]['cloud'] = w['cloud_cover'] cloud = w['cloud_cover'] d[e]['weather_api'] = True data_changed = True if production is not None and daylight is not None and cloud is not None: print('%s' % e, ' %s %.1f hrs %d%%' % \ (show_with_units(production), daylight, cloud)) else: print( '%s' % e, ' %s' % show_with_units(production)) print( '\nSign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' ) print( '\nFollow <a href="https://twitter.com/teslaliving">@TeslaLiving</a>.' ) print('\n<ul>') print( '<li><i>Note 1: Detailed generation tracking started 20150612.</i></li>' ) print('<li><i>Note 2: Cloud/Daylight data <a href="http://forecast.io">Powered by Forecast</a> when ' \ 'SolarCity data missing.</i></li>') print( '<li><i>Note 3: System was degraded from 20170530 to 20170726. Up to 30 panels offline.</i></li>' ) print('</ul>') if data_changed: save_data(data) log.debug("--- solarcity.py end ---")
def get_current_day_data(): driver = webdriver.Chrome() driver.implicitly_wait(30) try: driver.get('https://login.solarcity.com/logout') except: pass time.sleep(10) driver.get(AUTH_URL) time.sleep(10) driver.find_element_by_id("username").send_keys(SOLARCITY_USER) password = driver.find_element_by_id("password") password.send_keys(SOLARCITY_PASSWORD) password.submit() time.sleep(10) driver.find_element_by_xpath( "//div[@id='HomeCtrlView']/div[2]/div/div/a").click() production = 0 daylight_hours = 0 cloud_cover = 0 loops = 1 while loops > 0: time.sleep(10) data = driver.find_element_by_css_selector( "div.consumption-production-panel").text data += driver.find_element_by_css_selector( "div.details-panel.pure-g").text log.debug("raw data: %r", data) fields = data.split("\n") for f in fields: if f.find("hrs") != -1: try: daylight_hours = float(f.split()[0]) continue except: pass if f.find(" %") != -1: try: cloud_cover = int(f.split()[0]) continue except: pass if f.find("kWh") != -1: try: production = float(f.split()[0]) continue except: pass if production != 0: break loops -= 1 if float(production) == 0 and cloud_cover == 0 and daylight_hours == 0: raise Exception( "Problem getting current production level: %.1f, %d, %.1f" % (production, cloud_cover, daylight_hours)) if daylight_hours == 0: """ SolarCity has had outages where they cant provide the cloud cover/weather information. If the weather data appears empty here, we'll go get it from another source """ w = get_daytime_weather_data(log, time.time()) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] try: driver.find_element_by_xpath( "//ul[@id='mysc-nav']/li[18]/a/span").click() except: pass time.sleep(2) # If we get here everything worked, shut down the browser driver.quit() if os.path.exists("geckodriver.log"): os.remove("geckodriver.log") return daylight_hours, cloud_cover, production
def main(): parser = argparse.ArgumentParser(description='SolarCity Reporting') parser.add_argument('--no_email', help='Dont send emails', required=False, action='store_true') parser.add_argument('--force', help='Force update', required=False, action='store_true') parser.add_argument('--no_tweet', help='Dont post tweets', required=False, action='store_true') parser.add_argument('--report', help='Generate report', required=False, action='store_true') parser.add_argument('--blog', help='Generate html report page', required=False, action='store_true') parser.add_argument('--stocktweet', help='Tweet current SolarCity stock price', required=False, action='store_true') parser.add_argument('--daily', help='Report on daily generation', required=False, action='store_true') parser.add_argument('--monthly', help='Report on monthly generation', required=False, action='store_true') parser.add_argument('--yearly', help='Report on yearly generation', required=False, action='store_true') parser.add_argument('--weather', help='Report weather for given date (YYYYMMDD)', required=False, type=str) parser.add_argument('--pvoutput', help="Send data for date (YYYYMMDD) to PVOutput.org", required=False, type=str) args = parser.parse_args() if args.stocktweet: quote = get_stock_quote(stock='SCTY', log=log) if quote and not DEBUG_MODE: tweet_price(price=quote, log=log, stock='SCTY', extra='http://share.solarcity.com/teslaliving #GoSolar', image="images/TeslalivingLogo.jpg") elif quote: print "Would tweet price: $%s" % quote return # Make sure we only run one instance at a time fp = open('/tmp/solarcity.lock', 'w') try: fcntl.flock(fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except: log.debug("Sorry, someone else is running this tool right now. Please try again later") return -1 log.debug("--- solarcity.py start ---") data = load_data() data_changed = False data = load_historical_data(data) production_max, production_min, total_generation = analyze_data(data) if args.monthly: # First check if its last day of the month log.debug("Check for monthly update") now = datetime.datetime.now() dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of month.") current_month = time.strftime("%Y%m") if 'lastmonthlytweet' not in data['config'] or data['config']['lastmonthlytweet'] != current_month: tweet_month(data) data['config']['lastmonthlytweet'] = current_month data_changed = True else: log.debug(" Not last day of month, skipping. ") if args.yearly: # First check if its last day of the year log.debug("Check for annual update") now = datetime.datetime.now() if now.month == 12: dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of year.") current_month = time.strftime("%Y%m") if 'lastyearlytweet' not in data['config'] or data['config']['lastyearlytweet'] != current_month: tweet_year(data) data['config']['lastyearlytweet'] = current_month data_changed = True else: log.debug(" Not last day of year, skipping. ") if args.weather is not None: log.debug("Check weather data") if int(args.weather) == 0: time_value = int(time.time()) else: time_value = time.mktime(time.strptime("%s2100" % args.weather, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) print "Weather as of %s:" % datetime.datetime.fromtimestamp(time_value) print " Average temperature: %.1fF" % w["avg_temp"] print " Low temperature: %.1fF" % w["low_temp"] print " Cloud Cover: %d%%" % w["cloud_cover"] print " Daylight hours: %.1f" % w["daylight"] print " Description: %s" % w["description"] print " Precipitation type: %s" % w["precip_type"] print " Precipitation Chance: %d%%" % w["precip_probability"] # analyze_weather(data) if args.pvoutput is not None: if int(args.pvoutput) == 0: print "Uploading historical data to pvoutput.org" for d in data["data"]: print " Processing date %s" % d try: upload_to_pvoutput(data, d) except: print " problem with date %s" % d print " Sleeping" # You'll need longer sleeps if you didnt donate time.sleep(15) else: upload_to_pvoutput(data, args.pvoutput) if args.daily: log.debug("Check for daily update") current_day = time.strftime("%Y%m%d") if data['config']['lastdailytweet'] != current_day or DEBUG_MODE or args.force: daylight_hours, cloud_cover, production = get_current_day_data() data['data'][current_day] = {'daylight': daylight_hours, 'cloud': cloud_cover, 'production': production} special = None if production > data['data'][production_max]['production']: special = "high" if production < data['data'][production_min]['production']: special = "low" tweet_production(daylight_hours, cloud_cover, production, special) data['config']['lastdailytweet'] = current_day data_changed = True # Now upload to pvoutput upload_to_pvoutput(data, current_day) if args.report: # Send/Run weekly solarcity summary report solarcity_report(data, args.no_email, args.no_tweet) if args.blog: # Export all entries found for posting to static page on blog: teslaliving.net/solarcity log.debug("Reporting on SolarCity generation for blog") print "@SolarCity system size is 69 panels at 255W each = %s" % show_with_units(69 * .255) print "%s total power generated via @SolarCity as of %s" % (show_with_units(total_generation), time.strftime("%Y%m%d")) print "%s day max on %s" % (show_with_units(data['data'][production_max]['production']), production_max) print "%s day min on %s" % (show_with_units(data['data'][production_min]['production']), production_min) print "%s average production" % (show_with_units(total_generation / len(data['data']))) print 'Sign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' print "\n" print "%s%s%s %s %s" % ("Date", " " * 11, "Production", "Daylight", "Cloud Cover") d = data['data'] for e in sorted(d, reverse=True): production = d[e]['production'] if 'daylight' in d[e] and d[e]['daylight'] > 0: daylight = d[e]['daylight'] if 'cloud' in d[e]: cloud = d[e]['cloud'] else: cloud = 0 else: w = get_daytime_weather_data(log, time.mktime(time.strptime("%s2100" % e, "%Y%m%d%H%M"))) d[e]['daylight'] = w['daylight'] daylight = w['daylight'] d[e]['weather_api'] = True d[e]['cloud'] = w['cloud_cover'] cloud = w['cloud_cover'] d[e]['weather_api'] = True data_changed = True if production is not None and daylight is not None and cloud is not None: print '%s' % e, ' %s %.1f hrs %d%%' % \ (show_with_units(production), daylight, cloud) else: print '%s' % e, ' %s' % show_with_units(production) print '\nSign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' print '\nFollow <a href="https://twitter.com/teslaliving">@TeslaLiving</a>.' print '\n<ul>' print '<li><i>Note 1: Detailed generation tracking started 20150612.</i></li>' print '<li><i>Note 2: Cloud/Daylight data <a href="http://forecast.io">Powered by Forecast</a> when ' \ 'SolarCity data missing.</i></li>' print '</ul>' print '<a href="https://gratipay.com/teslaliving/"><img src="//img.shields.io/gratipay/teslaliving.svg"></a>' if data_changed: save_data(data) log.debug("--- solarcity.py end ---")
def get_current_day_data(): display = Display(visible=0, size=(800, 600)) display.start() # options = Options() # options.set_headless(headless=True) # driver = webdriver.Firefox(firefox_options=options, executable_path='/usr/local/bin/geckodriver') #binary = FirefoxBinary('/usr/local/geckodriver') driver = webdriver.Firefox(executable_path='/usr/local/bin/geckodriver') driver.implicitly_wait(30) # print("Invoked") # driver.get('http://google.com') try: driver.get('https://login.solarcity.com/logout') except: pass # print driver.title time.sleep(10) driver.get(AUTH_URL) # print driver.title time.sleep(10) # print os.environ["SOLARCITY_USER"] # print SOLARCITYUSER driver.find_element_by_id("username").send_keys(SOLARCITYUSER) password = driver.find_element_by_id("password") password.send_keys(SOLARCITYPASSWORD) password.submit() time.sleep(10) driver.find_element_by_xpath("//div[@id='HomeCtrlView']/div[2]/div/div/a").click() production = 0 daylight_hours = 0 cloud_cover = 0 loops = 1 # print driver.title while loops > 0: time.sleep(10) data = driver.find_element_by_css_selector("div.consumption-production-panel").text data += driver.find_element_by_css_selector("div.details-panel.pure-g").text log.debug("raw data: %r", data) fields = data.split("\n") for f in fields: if f.find("hrs") != -1: try: daylight_hours = float(f.split()[0]) continue except: pass if f.find(" %") != -1: try: cloud_cover = int(f.split()[0]) continue except: pass if f.find("kWh") != -1: try: production = float(f.split()[0]) continue except: pass if production != 0: break loops -= 1 if float(production) == 0 and cloud_cover == 0 and daylight_hours == 0: raise Exception("Problem getting current production level: %.1f, %d, %.1f" % (production, cloud_cover, daylight_hours)) if daylight_hours == 0: """ SolarCity has had outages where they cant provide the cloud cover/weather information. If the weather data appears empty here, we'll go get it from another source """ w = get_daytime_weather_data(log, time.time()) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] try: driver.find_element_by_xpath("//ul[@id='mysc-nav']/li[18]/a/span").click() except: pass time.sleep(2) # If we get here everything worked, shut down the browser driver.quit() display.stop() #print 'Success' return daylight_hours, cloud_cover, production
def get_current_day_data(): seleniumHost = 'localhost' seleniumPort = str(4444) browserStartCommand = "*firefox" browserURL = "https://*****:*****@id='HomeCtrlView']/div[2]/div/div/a") production = 0 daylight_hours = 0 cloud_cover = 0 loops = 1 while loops > 0: time.sleep(10) data = s.get_text("css=div.consumption-production-panel") data += s.get_text("css=div.details-panel.pure-g") # hist-summary.pure-g" log.debug("raw data: %r", data) fields = data.split("\n\n") for f in fields: if f.find("daylight") != -1: for o in f.split(): try: daylight_hours = float(o) break except: pass if f.find("cloud cover") != -1: for o in f.split(): try: cloud_cover = int(o) break except: pass if f.find("produced") != -1: for o in f.split(): try: production = float(o) break except: pass if production != 0: break loops -= 1 if float(production) == 0 and cloud_cover == 0 and daylight_hours == 0: raise Exception("Problem getting current production level") if daylight_hours == 0: """ SolarCity has had outages where they cant provide the cloud cover/weather information. If the weather data appears empty here, we'll go get it from another source """ w = get_daytime_weather_data(log, time.time()) cloud_cover = w["cloud_cover"] daylight_hours = w["daylight"] try: s.click("//ul[@id='mysc-nav']/li[18]/a/span") except: pass time.sleep(2) # If we get here everything worked, shut down the browser s.stop() return daylight_hours, cloud_cover, production
def main(): parser = argparse.ArgumentParser(description='Tesla Control') parser.add_argument('--status', help='Get car status', required=False, action='store_true') parser.add_argument('--mileage', help='Check car mileage and tweet as it crosses 1,000 mile marks', required=False, action='store_true') parser.add_argument('--state', help='Record car state', required=False, action='store_true') parser.add_argument('--pluggedin', help='Check if car is plugged in', required=False, action='store_true') parser.add_argument('--dump', help='Dump all fields/data', required=False, action='store_true') parser.add_argument('--fields', help='Check for newly added API fields', required=False, action='store_true') parser.add_argument('--day', help='Show state data for given day', required=False, type=str) parser.add_argument('--yesterday', help='Report on yesterdays driving', required=False, action='store_true') parser.add_argument('--export', help='Export data', required=False, action='store_true') parser.add_argument('--report', help='Produce summary report', required=False, action='store_true') parser.add_argument('--garage', help='Trigger garage door (experimental)', required=False, action='store_true') parser.add_argument('--sunroof', help='Control sunroof (vent, open, close)', required=False, type=str) args = parser.parse_args() # Make sure we only run one instance at a time blocked = True while blocked: fp = open('/tmp/tesla.lock', 'w') try: fcntl.flock(fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) blocked = False except: logT.debug("Someone else is running this tool right now. Sleeping") time.sleep(30) logT.debug("--- tesla.py start ---") data = load_data() data_changed = False # Get a connection to the car and manage access token if 'token' in data: token = data['token'] else: token = None try: c = establish_connection(token) except: logT.debug("Problems establishing connection") c = establish_connection() if c.access_token: if not 'token' in data or data['token'] != c.access_token: data['token'] = c.access_token data_changed = True if args.status: # Dump current Tesla status print dump_current_tesla_status(c) elif args.dump: # Dump all of Tesla API state information to disk logT.debug("Dumping current Tesla state") t = datetime.date.today() ts = t.strftime("%Y%m%d") m = dump_current_tesla_status(c) open(os.path.join(DUMP_DIR, "tesla_state_%s.txt" % ts), "w").write(m) elif args.fields: # Check for new Tesla API fields and report if any found logT.debug("Checking Tesla API fields") data_changed, data = check_tesla_fields(c, data) elif args.mileage: # Tweet mileage as it crosses 1,000 mile marks m = get_odometer(c, CAR_NAME) if "mileage_tweet" not in data: data["mileage_tweet"] = 0 if int(m / 1000) > int(data["mileage_tweet"] / 1000): tweet_major_mileage(int(m / 1000) * 1000) data["mileage_tweet"] = m data_changed = True elif args.state: # Save current Tesla state information logT.debug("Saving Tesla state") s = get_current_state(c, CAR_NAME) t = datetime.date.today() ts = t.strftime("%Y%m%d") hour = datetime.datetime.now().hour if hour < 12: ampm = "am" else: ampm = "pm" data["daily_state_%s" % ampm][ts] = s data_changed = True elif args.day: # Show Tesla state information from a given day ts = args.day if ts in data["daily_state_am"]: print "Data for %s am:" % ts for i in ("odometer", "soc", "ideal_range", "rated_range", "estimated_range", "charge_energy_added", "charge_miles_added_ideal", "charge_miles_added_rated"): print "%s: %s" % (i, data["daily_state_am"][ts][i]) elif args.report: # Show total and average energy added total_energy_added = 0 for ts in data["daily_state_am"]: if ts < "20151030": continue total_energy_added += data["daily_state_am"][ts]["charge_energy_added"] print "Total Energy Added: %s kW" % "{:,.2f}".format(total_energy_added) print "Average Energy Added: %s kW" % "{:,.2f}".format((total_energy_added / len(data["daily_state_am"]))) elif args.export: # Export all saved Tesla state information for ts in sorted(data["daily_state_am"]): if ts < "20151030": continue print "%s," % ts, for i in ("odometer", "soc", "ideal_range", "rated_range", "estimated_range", "charge_energy_added", "charge_miles_added_ideal", "charge_miles_added_rated"): print "%s," % data["daily_state_am"][ts][i], print "" elif args.pluggedin: # Check if the Tesla is plugged in and alert if not logT.debug("Checking if Tesla is plugged in") if not is_plugged_in(c, CAR_NAME): s = get_current_state(c, CAR_NAME, include_temps=False) message = "Your car is not plugged in.\n\n" message += "Current battery level is %d%%. (%d estimated miles)" % (s["soc"], int(s["estimated_range"])) message += "\n\nRegards,\nRob" email(email=TESLA_EMAIL, message=message, subject="Your Tesla isn't plugged in") elif args.yesterday: # Report on yesterdays mileage/efficiency t = datetime.date.today() today_ts = t.strftime("%Y%m%d") t = t + datetime.timedelta(days=-1) yesterday_ts = t.strftime("%Y%m%d") if today_ts not in data["daily_state_am"] or yesterday_ts not in data["daily_state_am"]: logT.debug("Skipping yesterday tweet due to missing items") else: miles_driven = data["daily_state_am"][today_ts]["odometer"] - data["daily_state_am"][yesterday_ts][ "odometer"] kw_used = data["daily_state_am"][today_ts]["charge_energy_added"] if miles_driven > 200: m = "Yesterday I drove my #Tesla %s miles on a road trip! " \ "@Teslamotors #bot" % ("{:,}".format(int(miles_driven))) elif miles_driven == 0: mileage = data["daily_state_am"][today_ts]["odometer"] today_ym = datetime.date.today() start_ym = datetime.date(2014, 4, 21) ownership_months = int((today_ym - start_ym).days / 30) m = "Yesterday my #Tesla had a day off. Current mileage is %s miles after %d months " \ "@Teslamotors #bot" % ("{:,}".format(int(mileage)), ownership_months) else: day = yesterday_ts time_value = time.mktime(time.strptime("%s2100" % day, "%Y%m%d%H%M")) w = get_daytime_weather_data(logT, time_value) m = "Yesterday I drove my #Tesla %s miles using %.1f kW with an effic. of %d Wh/mi. Avg temp %.1fF. " \ "@Teslamotors #bot" \ % ("{:,}".format(int(miles_driven)), kw_used, kw_used * 1000 / miles_driven, w["avg_temp"]) pic = random.choice(get_pics()) if DEBUG_MODE: print "Would tweet:\n%s with pic: %s" % (m, pic) logT.debug("DEBUG mode, not tweeting: %s with pic: %s", m, pic) else: logT.info("Tweeting: %s with pic: %s", m, pic) tweet_string(message=m, log=logT, media=pic) elif args.garage: # Open garage door (experimental as I dont have an AP car) trigger_garage_door(c, CAR_NAME) elif args.sunroof: # Change sunroof state trigger_sunroof(c, CAR_NAME, args.sunroof) if data_changed: save_data(data) logT.debug("--- tesla.py end ---")
def main(): parser = argparse.ArgumentParser(description='SolarCity Reporting') parser.add_argument('--no_email', help='Dont send emails', required=False, action='store_true') parser.add_argument('--force', help='Force update', required=False, action='store_true') parser.add_argument('--no_tweet', help='Dont post tweets', required=False, action='store_true') parser.add_argument('--report', help='Generate report', required=False, action='store_true') parser.add_argument('--blog', help='Generate html report page', required=False, action='store_true') parser.add_argument('--stocktweet', help='Tweet current SolarCity stock price', required=False, action='store_true') parser.add_argument('--daily', help='Report on daily generation', required=False, action='store_true') parser.add_argument('--monthly', help='Report on monthly generation', required=False, action='store_true') parser.add_argument('--yearly', help='Report on yearly generation', required=False, action='store_true') parser.add_argument('--weather', help='Report weather for given date (YYYYMMDD)', required=False, type=str) parser.add_argument('--pvoutput', help="Send data for date (YYYYMMDD) to PVOutput.org", required=False, type=str) args = parser.parse_args() dbconnecting() if args.stocktweet: quote = get_stock_quote(stock='SCTY', log=log) if quote and not DEBUG_MODE: tweet_price(price=quote, log=log, stock='SCTY', extra='http://share.solarcity.com/teslaliving #GoSolar', image="images/TeslalivingLogo.jpg") elif quote: print "Would tweet price: $%s" % quote return # Make sure we only run one instance at a time fp = open('/tmp/solarcity.lock', 'w') try: fcntl.flock(fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except: log.debug("Sorry, someone else is running this tool right now. Please try again later") return -1 log.debug("--- solarcity.py start ---") data = load_data() data_changed = False data = load_historical_data(data) production_max, production_min, total_generation = analyze_data(data) if args.monthly: # First check if its last day of the month log.debug("Check for monthly update") now = datetime.datetime.now() dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of month.") current_month = time.strftime("%Y%m") if 'lastmonthlytweet' not in data['config'] or data['config']['lastmonthlytweet'] != current_month: tweet_month(data) data['config']['lastmonthlytweet'] = current_month data_changed = True else: log.debug(" Not last day of month, skipping. ") if args.yearly: # First check if its last day of the year log.debug("Check for annual update") now = datetime.datetime.now() if now.month == 12: dow, last_day = calendar.monthrange(now.year, now.month) log.debug("Last day of month: %d. Current day: %d", last_day, now.day) if last_day == now.day: log.debug(" Last day of year.") current_month = time.strftime("%Y%m") if 'lastyearlytweet' not in data['config'] or data['config']['lastyearlytweet'] != current_month: tweet_year(data) data['config']['lastyearlytweet'] = current_month data_changed = True else: log.debug(" Not last day of year, skipping. ") if args.weather is not None: log.debug("Check weather data") if int(args.weather) == 0: time_value = int(time.time()) else: time_value = time.mktime(time.strptime("%s2100" % args.weather, "%Y%m%d%H%M")) w = get_daytime_weather_data(log, time_value) print "Weather as of %s:" % datetime.datetime.fromtimestamp(time_value) print " Average temperature: %.1fF" % w["avg_temp"] print " Low temperature: %.1fF" % w["low_temp"] print " Cloud Cover: %d%%" % w["cloud_cover"] print " Daylight hours: %.1f" % w["daylight"] print " Description: %s" % w["description"] print " Precipitation type: %s" % w["precip_type"] print " Precipitation Chance: %d%%" % w["precip_probability"] # analyze_weather(data) if args.pvoutput is not None: if int(args.pvoutput) == 0: print "Uploading historical data to pvoutput.org" for d in data["data"]: print " Processing date %s" % d try: upload_to_pvoutput(data, d) except: print " problem with date %s" % d print " Sleeping" # You'll need longer sleeps if you didnt donate time.sleep(15) else: upload_to_pvoutput(data, args.pvoutput) if args.daily: log.debug("Check for daily update") current_day = time.strftime("%Y%m%d") if data['config']['lastdailytweet'] != current_day or DEBUG_MODE or args.force: daylight_hours, cloud_cover, production = get_current_day_data() data['data'][current_day] = {'daylight': daylight_hours, 'cloud': cloud_cover, 'production': production} try: curs.execute ("INSERT INTO production values("+current_day+","+str(production)+","+str(daylight_hours)+","+str(cloud_cover)+")") db.commit() log.debug("Sent daily production to database") except Exception as exp: log.debug(str(exp)) db.rollback() special = None if production > data['data'][production_max]['production']: special = "high" if production < data['data'][production_min]['production']: special = "low" # tweet_production(daylight_hours, cloud_cover, production, special) data['config']['lastdailytweet'] = current_day data_changed = True # Now upload to pvoutput # upload_to_pvoutput(data, current_day) if args.report: # Send/Run weekly solarcity summary report solarcity_report(data, args.no_email, args.no_tweet) print data[0] if args.blog: # Export all entries found for posting to static page on blog: teslaliving.net/solarcity log.debug("Reporting on SolarCity generation for blog") print '<a href="http://share.solarcity.com/teslaliving">@SolarCity</a> Solar Installation' print '<h3>System Results</h3>' print "<b>%s total power generated via @SolarCity as of %s</b>" % (show_with_units(total_generation), time.strftime("%Y%m%d")) print "%s day max on %s" % (show_with_units(data['data'][production_max]['production']), production_max) print "%s day min on %s" % (show_with_units(data['data'][production_min]['production']), production_min) print "<b>%s daily average production</b>" % (show_with_units(total_generation / len(data['data']))) print '<h3>System Details</h3>' print "System size is 69 panels at 255W each = %.1fkW" % (69 * .255) r = relativedelta(datetime.datetime.now(), datetime.datetime.strptime("2015-02-23", '%Y-%m-%d')) elapsed_months = r.years * 12 + r.months print "System was turned on February 23, 2015 (%d months ago)" % elapsed_months print "Panel info: " print "* Size: 1638 x 982 x 40mm (64.5 x 38.7 x 1.57in)" print "* Vendor: <a href='http://www.canadiansolar.com/solar-panels/cs6p-p.html'>CanadianSolar CS6P-P</a>" print "Inverter info: " print "* <a href='http://www.solaredge.com/sites/default/files/se-single-phase-us-inverter-datasheet.pdf'>SolarEdge SE6000A</a>" print " " print 'Sign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' print '<h3>Chart via <a href="http://pvoutput.org/list.jsp?id=48753&sid=44393">PVOutput</a></h3>[hoops name="pvoutput"]' print '<h3>Daily Log:</h3>' print "%s%s%s %s %s" % ("Date", " " * 11, "Production", "Daylight", "Cloud Cover") d = data['data'] for e in sorted(d, reverse=True): production = d[e]['production'] if 'daylight' in d[e] and d[e]['daylight'] > 0: daylight = d[e]['daylight'] if 'cloud' in d[e]: cloud = d[e]['cloud'] else: cloud = 0 else: w = get_daytime_weather_data(log, time.mktime(time.strptime("%s2100" % e, "%Y%m%d%H%M"))) d[e]['daylight'] = w['daylight'] daylight = w['daylight'] d[e]['weather_api'] = True d[e]['cloud'] = w['cloud_cover'] cloud = w['cloud_cover'] d[e]['weather_api'] = True data_changed = True if production is not None and daylight is not None and cloud is not None: print '%s' % e, ' %s %.1f hrs %d%%' % \ (show_with_units(production), daylight, cloud) else: print '%s' % e, ' %s' % show_with_units(production) print '\nSign up for <a href="http://share.solarcity.com/teslaliving">SolarCity</a> and save on electric!' print '\nFollow <a href="https://twitter.com/teslaliving">@TeslaLiving</a>.' print '\n<ul>' print '<li><i>Note 1: Detailed generation tracking started 20150612.</i></li>' print '<li><i>Note 2: Cloud/Daylight data <a href="http://forecast.io">Powered by Forecast</a> when ' \ 'SolarCity data missing.</i></li>' print '<li><i>Note 3: System was degraded from 20170530 to 20170726. Up to 30 panels offline.</i></li>' print '</ul>' if data_changed: save_data(data) log.debug("--- solarcity.py end ---")