Can you try this version of the belchertown.py file? Replace your current
one, restart weewx and see if it helps?
If it does I'll push it to GitHub
On Tuesday, June 2, 2020 at 8:44:01 AM UTC-4, NanoG5Kite wrote:
>
> Yes, Python 3 - and many thanks! Will test further as soon a fix available.
>
> br,
>
> Matthias
>
> Am Dienstag, 2. Juni 2020 14:36:10 UTC+2 schrieb Pat:
>>
>> That's helpful. Looks like an encoding issue. You're using Python 3
>> right?
>>
>> I'll try and push a fix to the 1.2 development branch soon
>>
>> On Tuesday, June 2, 2020 at 8:07:43 AM UTC-4, NanoG5Kite wrote:
>>>
>>> Still the same:
>>>
>>>
>>> https://api.aerisapi.com/forecasts/54.00318,9.76869?&format=json&filter=day&limit=7&client_id=dT1oxxxxx_secret=aHcxxxxxxx,
>>>
>>> and the error is: the JSON object must be str, not 'bytes'
>>>
>>> error:
>>>
>>> the JSON object must be str, not 'bytes' ???
>>>
>>> The link itself pasted into my browser is working, provides enclosed
>>> zipped jason...
>>>
>>>
>>>
>>>
>>>
>>> Am Dienstag, 2. Juni 2020 13:45:50 UTC+2 schrieb NanoG5Kite:
>>>>
>>>> Hi Pat,
>>>>
>>>> many thanks for supporting me!!!
>>>>
>>>> Had first to install rsyslog to get the right log - pls. see enclosed -
>>>> hope the error becomes clear now?
>>>> Maybe my Longitude "54" needs extra digits as e.g. 54.0000? Will also
>>>> give this s a try.
>>>> My secrets/keys I have shorten with xxxxxx....
>>>>
>>>>
>>>>
>>>>
>>>>
>>>>
>>>> Am Dienstag, 2. Juni 2020 12:45:58 UTC+2 schrieb Pat:
>>>>>
>>>>> Can you show me the full log? It's being truncated
>>>>>
>>>>> Hint: Some lines were ellipsized, use -l to show in full.
>>>>>
>>>>> On Tuesday, June 2, 2020 at 4:10:50 AM UTC-4, NanoG5Kite wrote:
>>>>>>
>>>>>>
>>>>>> Added a 2nd key on Aeris, also this counting down all 5 minutes, but
>>>>>> no luck...
>>>>>>
>>>>>>
>>>>>> Am Dienstag, 2. Juni 2020 09:50:38 UTC+2 schrieb NanoG5Kite:
>>>>>>>
>>>>>>> That will be hard to discover now - the keys should be correct as
>>>>>>> Aeris is counting the requests.
>>>>>>>
>>>>>>> My dev.version:
>>>>>>>
>>>>>>> - Station hardware: ecowitt-client
>>>>>>> - Server uptime: 1 day, 20 hours, 38 minutes
>>>>>>> - WeeWX uptime: 0 days, 1 hour, 31 minutes
>>>>>>> - WeeWX version: 4.0.0
>>>>>>> - Belchertown Skin Version: 1.2b4
>>>>>>>
>>>>>>> But will check later today again...
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> Am Dienstag, 2. Juni 2020 08:57:28 UTC+2 schrieb Pat:
>>>>>>>>
>>>>>>>> That's an invalid url error. Check that your keys are correct and
>>>>>>>> you are on the latest development version of the skin.
>>>>>>>
>>>>>>>
--
You received this message because you are subscribed to the Google Groups
"weewx-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/weewx-user/41462ba2-a151-44a0-a926-7d6f5e078ed4%40googlegroups.com.
# Extension for the Belchertown skin.
# This extension builds search list extensions as well
# as a crude "cron" to download necessary files.
#
# Pat O'Brien, August 19, 2018
from __future__ import with_statement
from __future__ import print_function # Python 2/3 compatibility
import datetime
import time
import calendar
import json
import os
import os.path
import syslog
import sys
import locale
import weewx
import weecfg
import configobj
import weedb
import weeutil.weeutil
import weewx.reportengine
import weewx.station
import weewx.units
import weewx.tags
import weeplot.genplot
import weeplot.utilities
from collections import OrderedDict
from weewx.cheetahgenerator import SearchList
from weewx.tags import TimespanBinder
from weeutil.weeutil import to_bool, TimeSpan, to_float, to_int, archiveDaySpan, archiveWeekSpan, archiveMonthSpan, archiveYearSpan, startOfDay, timestamp_to_string, option_as_list
try:
from weeutil.config import search_up
except:
# Pass here because chances are we have an old version of weewx which will get caught below.
pass
try:
# weewx 4
from weeutil.config import accumulateLeaves
except:
# weewx 3
from weeutil.weeutil import accumulateLeaves
# Check weewx version. Many things like search_up, weeutil.weeutil.KeyDict (label_dict) are from 3.9
if weewx.__version__ < "3.9":
raise weewx.UnsupportedFeature("weewx 3.9 and newer is required, found %s" % weewx.__version__)
try:
# Test for new-style weewx v4 logging by trying to import weeutil.logger
import weeutil.logger
import logging
log = logging.getLogger(__name__)
def logdbg(msg):
log.debug(msg)
def loginf(msg):
log.info(msg)
def logerr(msg):
log.error(msg)
except ImportError:
# Old-style weewx logging
import syslog
def logmsg(level, msg):
syslog.syslog(level, 'Belchertown Extension: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
# Print version in syslog for easier troubleshooting
VERSION = "1.2b4"
loginf("version %s" % VERSION)
class getData(SearchList):
def __init__(self, generator):
SearchList.__init__(self, generator)
def get_extension_list(self, timespan, db_lookup):
"""
Build the data needed for the Belchertown skin
"""
# Look for the debug flag which can be used to show more logging
weewx.debug = int(self.generator.config_dict.get('debug', 0))
# Setup label dict for text and titles
try:
d = self.generator.skin_dict['Labels']['Generic']
except KeyError:
d = {}
label_dict = weeutil.weeutil.KeyDict(d)
# Setup database manager
binding = self.generator.config_dict['StdReport'].get('data_binding', 'wx_binding')
manager = self.generator.db_binder.get_manager(binding)
belchertown_debug = self.generator.skin_dict['Extras'].get('belchertown_debug', 0)
# Find the right HTML ROOT
if 'HTML_ROOT' in self.generator.skin_dict:
html_root = os.path.join(self.generator.config_dict['WEEWX_ROOT'],
self.generator.skin_dict['HTML_ROOT'])
else:
html_root = os.path.join(self.generator.config_dict['WEEWX_ROOT'],
self.generator.config_dict['StdReport']['HTML_ROOT'])
# Setup UTC offset hours for moment.js in index.html
moment_js_stop_struct = time.localtime( time.time() )
moment_js_utc_offset = (calendar.timegm(moment_js_stop_struct) - calendar.timegm(time.gmtime(time.mktime(moment_js_stop_struct))))/60
# Highcharts UTC offset is the opposite of normal. Positive values are west, negative values are east of UTC. https://api.highcharts.com/highcharts/time.timezoneOffset
# Multiplying by -1 will reverse the number sign and keep 0 (not -0). https://stackoverflow.com/a/14053631/1177153
highcharts_timezoneoffset = moment_js_utc_offset * -1
# If theme locale is auto, get the system locale for use with moment.js, and the system decimal for use with highcharts
if self.generator.skin_dict['Extras']['belchertown_locale'] == "auto":
system_locale, locale_encoding = locale.getdefaultlocale()
else:
try:
# Try setting the locale. Locale needs to be in locale.encoding format. Example: "en_US.UTF-8", or "de_DE.UTF-8"
locale.setlocale(locale.LC_ALL, self.generator.skin_dict['Extras']['belchertown_locale'])
system_locale, locale_encoding = locale.getlocale()
except Exception as error:
# The system can't find the locale requested, so just set the variables anyways for JavaScript's use.
system_locale, locale_encoding = self.generator.skin_dict['Extras']['belchertown_locale'].split(".")
if belchertown_debug:
logerr( "Locale: Error using locale %s. This locale may not be installed on your system and you may see unexpected results. Belchertown skin JavaScript will try to use this locale. Full error: %s" % ( self.generator.skin_dict['Extras']['belchertown_locale'], error ) )
if system_locale is None:
# Unable to determine locale. Fallback to en_US
system_locale = "en_US"
if locale_encoding is None:
# Unable to determine locale_encoding. Fallback to UTF-8
locale_encoding = "UTF-8"
try:
system_locale_js = system_locale.replace("_", "-") # Python's locale is underscore. JS uses dashes.
except:
system_locale_js = "en-US" # Error finding locale, set to en-US
highcharts_decimal = self.generator.skin_dict['Extras'].get('highcharts_decimal', None)
# Change the Highcharts decimal to the locale if the option is missing or on auto mode, otherwise use whats defined in Extras
if highcharts_decimal is None or highcharts_decimal == "auto":
try:
highcharts_decimal = locale.localeconv()["decimal_point"]
except:
# Locale not found, default back to a period
highcharts_decimal = "."
highcharts_thousands = self.generator.skin_dict['Extras'].get('highcharts_thousands', None)
# Change the Highcharts thousands separator to the locale if the option is missing or on auto mode, otherwise use whats defined in Extras
if highcharts_thousands is None or highcharts_thousands == "auto":
try:
highcharts_thousands = locale.localeconv()["thousands_sep"]
except:
# Locale not found, default back to a comma
highcharts_thousands = ","
# Get the archive interval for the highcharts gapsize
try:
archive_interval_ms = int(self.generator.config_dict["StdArchive"]["archive_interval"]) * 1000
except KeyError:
archive_interval_ms = 300000 # 300*1000 for archive_interval emulated to millis
# Get the ordinal labels
default_ordinate_names = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N/A']
try:
ordinate_names = weeutil.weeutil.option_as_list(self.generator.skin_dict['Units']['Ordinates']['directions'])
try:
ordinate_names = [unicode(x, "utf-8") for x in ordinate_names] # Python 2, convert to unicode
except:
pass
except KeyError:
ordinate_names = default_ordinate_names
# Build the chart array for the HTML
# Outputs a dict of nested lists which allow you to have different charts for different timespans on the site in different order with different names.
# OrderedDict([('day', ['chart1', 'chart2', 'chart3', 'chart4']),
# ('week', ['chart1', 'chart5', 'chart6', 'chart2', 'chart3', 'chart4']),
# ('month', ['this_is_chart1', 'chart2_is_here', 'chart3', 'windSpeed_and_windDir', 'chart5', 'chart6', 'chart7']),
# ('year', ['chart1', 'chart2', 'chart3', 'chart4', 'chart5'])])
chart_config_path = os.path.join(
self.generator.config_dict['WEEWX_ROOT'],
self.generator.skin_dict['SKIN_ROOT'],
self.generator.skin_dict.get('skin', ''),
'graphs.conf')
default_chart_config_path = os.path.join(
self.generator.config_dict['WEEWX_ROOT'],
self.generator.skin_dict['SKIN_ROOT'],
self.generator.skin_dict.get('skin', ''),
'graphs.conf.example')
if os.path.exists( chart_config_path ):
chart_dict = configobj.ConfigObj(chart_config_path, file_error=True)
else:
chart_dict = configobj.ConfigObj(default_chart_config_path, file_error=True)
charts = OrderedDict()
for chart_timespan in chart_dict.sections:
timespan_chart_list = []
for plotname in chart_dict[chart_timespan].sections:
if plotname not in timespan_chart_list:
timespan_chart_list.append( plotname )
charts[chart_timespan] = timespan_chart_list
# Create a dict of chart group titles for use on the graphs page header. If no title defined, use the chart group name
graphpage_titles = OrderedDict()
for chartgroup in chart_dict.sections:
if "title" in chart_dict[chartgroup]:
graphpage_titles[chartgroup] = chart_dict[chartgroup]["title"]
else:
graphpage_titles[chartgroup] = chartgroup
# Create a dict of chart group page content for use on the graphs page below the header.
graphpage_content = OrderedDict()
for chartgroup in chart_dict.sections:
if "page_content" in chart_dict[chartgroup]:
graphpage_content[chartgroup] = chart_dict[chartgroup]["page_content"]
# Setup the Graphs page button row based on the skin extras option and the button_text from graphs.conf
graph_page_buttons = ""
graph_page_graphgroup_buttons = []
for chartgroup in chart_dict.sections:
if "show_button" in chart_dict[chartgroup] and chart_dict[chartgroup]["show_button"].lower() == "true":
graph_page_graphgroup_buttons.append(chartgroup)
for gg in graph_page_graphgroup_buttons:
if "button_text" in chart_dict[gg]:
button_text = chart_dict[gg]["button_text"]
else:
button_text = gg
graph_page_buttons += '<a href="./?graph='+gg+'"><button type="button" class="btn btn-primary">' + button_text + '</button></a>'
graph_page_buttons += " " # Spacer between the button
# Set a default radar URL using station's lat/lon. Moved from skin.conf so we can get station lat/lon from weewx.conf. A lot of stations out there with Belchertown 0.1 through 0.7 are showing the visitor's location and not the proper station location because nobody edited the radar_html which did not have lat/lon set previously.
if self.generator.skin_dict['Extras']['radar_html'] == "":
lat = self.generator.config_dict['Station']['latitude']
lon = self.generator.config_dict['Station']['longitude']
radar_html = '<iframe width="650" height="360" src="https://embed.windy.com/embed2.html?lat={}&lon={}&zoom=8&level=surface&overlay=radar&menu=&message=true&marker=&calendar=&pressure=&type=map&location=coordinates&detail=&detailLat={}&detailLon={}&metricWind=&metricTemp=&radarRange=-1" frameborder="0"></iframe>'.format( lat, lon, lat, lon )
else:
radar_html = self.generator.skin_dict['Extras']['radar_html']
"""
Build the all time stats.
"""
wx_manager = db_lookup()
# Find the beginning of the current year
now = datetime.datetime.now()
date_time = '01/01/%s 00:00:00' % now.year
pattern = '%m/%d/%Y %H:%M:%S'
year_start_epoch = int(time.mktime(time.strptime(date_time, pattern)))
#_start_ts = startOfInterval(year_start_epoch ,86400) # This is the current calendar year
# Setup the converter
# Get the target unit nickname (something like 'US' or 'METRIC'):
target_unit_nickname = self.generator.config_dict['StdConvert']['target_unit']
# Get the target unit: weewx.US, weewx.METRIC, weewx.METRICWX
target_unit = weewx.units.unit_constants[target_unit_nickname.upper()]
# Bind to the appropriate standard converter units
converter = weewx.units.StdUnitConverters[target_unit]
# Temperature Range Lookups
# 1. The database query finds the result based off the total column.
# 2. We need to convert the min, max to the site's requested unit.
# 3. We need to re-calculate the min/max range because the unit may have changed.
year_outTemp_max_range_query = wx_manager.getSql( 'SELECT dateTime, ROUND( (max - min), 1 ) as total, ROUND( min, 1 ) as min, ROUND( max, 1 ) as max FROM archive_day_outTemp WHERE dateTime >= %s AND min IS NOT NULL AND max IS NOT NULL ORDER BY total DESC LIMIT 1;' % year_start_epoch )
year_outTemp_min_range_query = wx_manager.getSql( 'SELECT dateTime, ROUND( (max - min), 1 ) as total, ROUND( min, 1 ) as min, ROUND( max, 1 ) as max FROM archive_day_outTemp WHERE dateTime >= %s AND min IS NOT NULL AND max IS NOT NULL ORDER BY total ASC LIMIT 1;' % year_start_epoch )
at_outTemp_max_range_query = wx_manager.getSql( 'SELECT dateTime, ROUND( (max - min), 1 ) as total, ROUND( min, 1 ) as min, ROUND( max, 1 ) as max FROM archive_day_outTemp WHERE min IS NOT NULL AND max IS NOT NULL ORDER BY total DESC LIMIT 1;' )
at_outTemp_min_range_query = wx_manager.getSql( 'SELECT dateTime, ROUND( (max - min), 1 ) as total, ROUND( min, 1 ) as min, ROUND( max, 1 ) as max FROM archive_day_outTemp WHERE min IS NOT NULL AND max IS NOT NULL ORDER BY total ASC LIMIT 1;' )
# Find the group_name for outTemp
outTemp_unit = converter.group_unit_dict["group_temperature"]
# Find the number of decimals to round to
outTemp_round = self.generator.skin_dict['Units']['StringFormats'].get(outTemp_unit, "%.1f")
# Largest Daily Temperature Range Conversions
# Max temperature for this day
if year_outTemp_max_range_query is not None:
year_outTemp_max_range_max_tuple = (year_outTemp_max_range_query[3], outTemp_unit, 'group_temperature')
year_outTemp_max_range_max = outTemp_round % self.generator.converter.convert(year_outTemp_max_range_max_tuple)[0]
# Min temperature for this day
year_outTemp_max_range_min_tuple = (year_outTemp_max_range_query[2], outTemp_unit, 'group_temperature')
year_outTemp_max_range_min = outTemp_round % self.generator.converter.convert(year_outTemp_max_range_min_tuple)[0]
# Largest Daily Temperature Range total
year_outTemp_max_range_total = outTemp_round % ( float(year_outTemp_max_range_max) - float(year_outTemp_max_range_min) )
# Replace the SQL Query output with the converted values
year_outTemp_range_max = [ year_outTemp_max_range_query[0], locale.format("%g", float(year_outTemp_max_range_total)), locale.format("%g", float(year_outTemp_max_range_min)), locale.format("%g", float(year_outTemp_max_range_max)) ]
else:
year_outTemp_range_max = [ calendar.timegm( time.gmtime() ), locale.format("%.1f", 0), locale.format("%.1f", 0), locale.format("%.1f", 0) ]
# Smallest Daily Temperature Range Conversions
# Max temperature for this day
if year_outTemp_min_range_query is not None:
year_outTemp_min_range_max_tuple = (year_outTemp_min_range_query[3], outTemp_unit, 'group_temperature')
year_outTemp_min_range_max = outTemp_round % self.generator.converter.convert(year_outTemp_min_range_max_tuple)[0]
# Min temperature for this day
year_outTemp_min_range_min_tuple = (year_outTemp_min_range_query[2], outTemp_unit, 'group_temperature')
year_outTemp_min_range_min = outTemp_round % self.generator.converter.convert(year_outTemp_min_range_min_tuple)[0]
# Smallest Daily Temperature Range total
year_outTemp_min_range_total = outTemp_round % ( float(year_outTemp_min_range_max) - float(year_outTemp_min_range_min) )
# Replace the SQL Query output with the converted values
year_outTemp_range_min = [ year_outTemp_min_range_query[0], locale.format("%g", float(year_outTemp_min_range_total)), locale.format("%g", float(year_outTemp_min_range_min)), locale.format("%g", float(year_outTemp_min_range_max)) ]
else:
year_outTemp_range_min = [ calendar.timegm( time.gmtime() ), locale.format("%.1f", 0), locale.format("%.1f", 0), locale.format("%.1f", 0) ]
# All Time - Largest Daily Temperature Range Conversions
# Max temperature
at_outTemp_max_range_max_tuple = (at_outTemp_max_range_query[3], outTemp_unit, 'group_temperature')
at_outTemp_max_range_max = outTemp_round % self.generator.converter.convert(at_outTemp_max_range_max_tuple)[0]
# Min temperature for this day
at_outTemp_max_range_min_tuple = (at_outTemp_max_range_query[2], outTemp_unit, 'group_temperature')
at_outTemp_max_range_min = outTemp_round % self.generator.converter.convert(at_outTemp_max_range_min_tuple)[0]
# Largest Daily Temperature Range total
at_outTemp_max_range_total = outTemp_round % ( float(at_outTemp_max_range_max) - float(at_outTemp_max_range_min) )
# Replace the SQL Query output with the converted values
at_outTemp_range_max = [ at_outTemp_max_range_query[0], locale.format("%g", float(at_outTemp_max_range_total)), locale.format("%g", float(at_outTemp_max_range_min)), locale.format("%g", float(at_outTemp_max_range_max)) ]
# All Time - Smallest Daily Temperature Range Conversions
# Max temperature for this day
at_outTemp_min_range_max_tuple = (at_outTemp_min_range_query[3], outTemp_unit, 'group_temperature')
at_outTemp_min_range_max = outTemp_round % self.generator.converter.convert(at_outTemp_min_range_max_tuple)[0]
# Min temperature for this day
at_outTemp_min_range_min_tuple = (at_outTemp_min_range_query[2], outTemp_unit, 'group_temperature')
at_outTemp_min_range_min = outTemp_round % self.generator.converter.convert(at_outTemp_min_range_min_tuple)[0]
# Smallest Daily Temperature Range total
at_outTemp_min_range_total = outTemp_round % ( float(at_outTemp_min_range_max) - float(at_outTemp_min_range_min) )
# Replace the SQL Query output with the converted values
at_outTemp_range_min = [ at_outTemp_min_range_query[0], locale.format("%g", float(at_outTemp_min_range_total)), locale.format("%g", float(at_outTemp_min_range_min)), locale.format("%g", float(at_outTemp_min_range_max)) ]
# Rain lookups
# Find the group_name for rain
rain_unit = converter.group_unit_dict["group_rain"]
# Find the number of decimals to round to
rain_round = self.generator.skin_dict['Units']['StringFormats'].get(rain_unit, "%.2f")
# Rainiest Day
rainiest_day_query = wx_manager.getSql( 'SELECT dateTime, sum FROM archive_day_rain WHERE dateTime >= %s ORDER BY sum DESC LIMIT 1;' % year_start_epoch )
if rainiest_day_query is not None:
rainiest_day_tuple = (rainiest_day_query[1], rain_unit, 'group_rain')
rainiest_day_converted = rain_round % self.generator.converter.convert(rainiest_day_tuple)[0]
rainiest_day = [ rainiest_day_query[0], rainiest_day_converted ]
else:
rainiest_day = [ calendar.timegm( time.gmtime() ), locale.format("%.2f", 0) ]
# All Time Rainiest Day
at_rainiest_day_query = wx_manager.getSql( 'SELECT dateTime, sum FROM archive_day_rain ORDER BY sum DESC LIMIT 1' )
at_rainiest_day_tuple = (at_rainiest_day_query[1], rain_unit, 'group_rain')
at_rainiest_day_converted = rain_round % self.generator.converter.convert(at_rainiest_day_tuple)[0]
at_rainiest_day = [ at_rainiest_day_query[0], at_rainiest_day_converted ]
# Find what kind of database we're working with and specify the correctly tailored SQL Query for each type of database
data_binding = self.generator.config_dict['StdArchive']['data_binding']
database = self.generator.config_dict['DataBindings'][data_binding]['database']
database_type = self.generator.config_dict['Databases'][database]['database_type']
driver = self.generator.config_dict['DatabaseTypes'][database_type]['driver']
if driver == "weedb.sqlite":
year_rainiest_month_sql = 'SELECT strftime("%%m", datetime(dateTime, "unixepoch")) as month, ROUND( SUM( sum ), 2 ) as total FROM archive_day_rain WHERE strftime("%%Y", datetime(dateTime, "unixepoch")) = "%s" GROUP BY month ORDER BY total DESC LIMIT 1;' % time.strftime( "%Y", time.localtime( time.time() ) )
at_rainiest_month_sql = 'SELECT strftime("%m", datetime(dateTime, "unixepoch")) as month, strftime("%Y", datetime(dateTime, "unixepoch")) as year, ROUND( SUM( sum ), 2 ) as total FROM archive_day_rain GROUP BY month, year ORDER BY total DESC LIMIT 1;'
year_rain_data_sql = 'SELECT dateTime, ROUND( sum, 2 ) FROM archive_day_rain WHERE strftime("%%Y", datetime(dateTime, "unixepoch")) = "%s" AND count > 0;' % time.strftime( "%Y", time.localtime( time.time() ) )
# The all stats from http://www.weewx.com/docs/customizing.htm doesn't seem to calculate "Total Rainfall for" all time stat correctly.
at_rain_highest_year_sql = 'SELECT strftime("%Y", datetime(dateTime, "unixepoch")) as year, ROUND( SUM( sum ), 2 ) as total FROM archive_day_rain GROUP BY year ORDER BY total DESC LIMIT 1;'
elif driver == "weedb.mysql":
year_rainiest_month_sql = 'SELECT FROM_UNIXTIME( dateTime, "%%m" ) AS month, ROUND( SUM( sum ), 2 ) AS total FROM archive_day_rain WHERE year( FROM_UNIXTIME( dateTime ) ) = "{0}" GROUP BY month ORDER BY total DESC LIMIT 1;'.format( time.strftime( "%Y", time.localtime( time.time() ) ) ) # Why does this one require .format() but the other's don't?
at_rainiest_month_sql = 'SELECT FROM_UNIXTIME( dateTime, "%%m" ) AS month, FROM_UNIXTIME( dateTime, "%%Y" ) AS year, ROUND( SUM( sum ), 2 ) AS total FROM archive_day_rain GROUP BY month, year ORDER BY total DESC LIMIT 1;'
year_rain_data_sql = 'SELECT dateTime, ROUND( sum, 2 ) FROM archive_day_rain WHERE year( FROM_UNIXTIME( dateTime ) ) = "%s" AND count > 0;' % time.strftime( "%Y", time.localtime( time.time() ) )
# The all stats from http://www.weewx.com/docs/customizing.htm doesn't seem to calculate "Total Rainfall for" all time stat correctly.
at_rain_highest_year_sql = 'SELECT FROM_UNIXTIME( dateTime, "%%Y" ) AS year, ROUND( SUM( sum ), 2 ) AS total FROM archive_day_rain GROUP BY year ORDER BY total DESC LIMIT 1;'
# Rainiest month
year_rainiest_month_query = wx_manager.getSql( year_rainiest_month_sql )
if year_rainiest_month_query is not None:
year_rainiest_month_tuple = (year_rainiest_month_query[1], rain_unit, 'group_rain')
year_rainiest_month_converted = rain_round % self.generator.converter.convert(year_rainiest_month_tuple)[0]
# Python 2/3 hack
try:
year_rainiest_month_name = calendar.month_name[ int( year_rainiest_month_query[0] ) ].decode('utf-8') # Python 2
except:
year_rainiest_month_name = calendar.month_name[ int( year_rainiest_month_query[0] ) ]
year_rainiest_month = [ year_rainiest_month_name, locale.format("%g", float(year_rainiest_month_converted)) ]
else:
year_rainiest_month = [ "N/A", 0.0 ]
# All time rainiest month
at_rainiest_month_query = wx_manager.getSql( at_rainiest_month_sql )
at_rainiest_month_tuple = (at_rainiest_month_query[2], rain_unit, 'group_rain')
at_rainiest_month_converted = rain_round % self.generator.converter.convert(at_rainiest_month_tuple)[0]
# Python 2/3 hack
try:
at_rainiest_month_name = calendar.month_name[ int( at_rainiest_month_query[0] ) ].decode('utf-8') # Python 2
except:
at_rainiest_month_name = calendar.month_name[ int( at_rainiest_month_query[0] ) ]
at_rainiest_month = [
"%s, %s" % (at_rainiest_month_name, at_rainiest_month_query[1]),
locale.format("%g", float(at_rainiest_month_converted))
]
# All time rainiest year
at_rain_highest_year_query = wx_manager.getSql( at_rain_highest_year_sql )
at_rain_highest_year_tuple = (at_rain_highest_year_query[1], rain_unit, 'group_rain')
#at_rain_highest_year_converted = round( self.generator.converter.convert(at_rain_highest_year_tuple)[0], rain_round )
at_rain_highest_year_converted = rain_round % self.generator.converter.convert(at_rain_highest_year_tuple)[0]
at_rain_highest_year = [ at_rain_highest_year_query[0], locale.format("%g", float(at_rain_highest_year_converted)) ]
# Consecutive days with/without rainfall
# dateTime needs to be epoch. Conversion done in the template using #echo
year_days_with_rain_total = 0
year_days_without_rain_total = 0
year_days_with_rain_output = {}
year_days_without_rain_output = {}
year_rain_query = wx_manager.genSql( year_rain_data_sql )
for row in year_rain_query:
# Original MySQL way: CASE WHEN sum!=0 THEN @total+1 ELSE 0 END
if row[1] != 0:
year_days_with_rain_total += 1
else:
year_days_with_rain_total = 0
# Original MySQL way: CASE WHEN sum=0 THEN @total+1 ELSE 0 END
if row[1] == 0:
year_days_without_rain_total += 1
else:
year_days_without_rain_total = 0
year_days_with_rain_output[row[0]] = year_days_with_rain_total
year_days_without_rain_output[row[0]] = year_days_without_rain_total
if year_days_with_rain_output:
year_days_with_rain = max( zip( year_days_with_rain_output.values(), year_days_with_rain_output.keys() ) )
else:
year_days_with_rain = [ locale.format("%.1f", 0), calendar.timegm( time.gmtime() ) ]
if year_days_without_rain_output:
year_days_without_rain = max( zip( year_days_without_rain_output.values(), year_days_without_rain_output.keys() ) )
else:
year_days_without_rain = [ locale.format("%.1f", 0), calendar.timegm( time.gmtime() ) ]
at_days_with_rain_total = 0
at_days_without_rain_total = 0
at_days_with_rain_output = {}
at_days_without_rain_output = {}
at_rain_query = wx_manager.genSql( "SELECT dateTime, ROUND( sum, 2 ) FROM archive_day_rain WHERE count > 0;" )
for row in at_rain_query:
# Original MySQL way: CASE WHEN sum!=0 THEN @total+1 ELSE 0 END
if row[1] != 0:
at_days_with_rain_total += 1
else:
at_days_with_rain_total = 0
# Original MySQL way: CASE WHEN sum=0 THEN @total+1 ELSE 0 END
if row[1] == 0:
at_days_without_rain_total += 1
else:
at_days_without_rain_total = 0
at_days_with_rain_output[row[0]] = at_days_with_rain_total
at_days_without_rain_output[row[0]] = at_days_without_rain_total
if len(at_days_with_rain_output) > 0:
at_days_with_rain = max( zip( at_days_with_rain_output.values(), at_days_with_rain_output.keys() ) )
else:
at_days_with_rain = (0,0)
if len(at_days_without_rain_output) > 0:
at_days_without_rain = max( zip( at_days_without_rain_output.values(), at_days_without_rain_output.keys() ) )
else:
at_days_without_rain = (0,0)
"""
This portion is right from the weewx sample http://www.weewx.com/docs/customizing.htm
"""
all_stats = TimespanBinder( timespan,
db_lookup,
formatter=self.generator.formatter,
converter=self.generator.converter,
skin_dict=self.generator.skin_dict )
# Get the unit label from the skin dict for speed.
windSpeed_unit = self.generator.skin_dict["Units"]["Groups"]["group_speed"]
windSpeed_unit_label = self.generator.skin_dict["Units"]["Labels"][windSpeed_unit]
"""
Get NOAA Data
"""
years = []
noaa_header_html = ""
default_noaa_file = ""
noaa_dir = html_root + "/NOAA/"
try:
noaa_file_list = os.listdir( noaa_dir )
# Generate a list of years based on file name
for f in noaa_file_list:
filename = f.split(".")[0] # Drop the .txt
year = filename.split("-")[1]
years.append(year)
years = sorted( set( years ) )[::-1] # Remove duplicates with set, and sort numerically, then reverse sort with [::-1] oldest year last
#first_year = years[0]
#final_year = years[-1]
for y in years:
# Link to the year file
if os.path.exists( noaa_dir + "NOAA-%s.txt" % y ):
noaa_header_html += '<a href="?yr=%s" class="noaa_rep_nav"><b>%s</b></a>:' % ( y, y )
else:
noaa_header_html += '<span class="noaa_rep_nav"><b>%s</b></span>:' % y
# Loop through all 12 months and find if the file exists.
# If the file doesn't exist, just show the month name in the header without a href link.
# There is no month 13, but we need to loop to 12, so 13 is where it stops.
for i in range(1, 13):
month_num = format( i, '02' ) # Pad the number with a 0 since the NOAA files use 2 digit month
month_abbr = calendar.month_abbr[ i ]
if os.path.exists( noaa_dir + "NOAA-%s-%s.txt" % ( y, month_num ) ):
noaa_header_html += ' <a href="?yr=%s&mo=%s" class="noaa_rep_nav"><b>%s</b></a>' % ( y, month_num, month_abbr )
else:
noaa_header_html += ' <span class="noaa_rep_nav"><b>%s</b></span>' % month_abbr
# Row build complete, push next row to new line
noaa_header_html += "<br>"
# Find the current month's NOAA file for the default file to show on JavaScript page load.
# The NOAA files are generated as part of this skin, but if for some reason that the month file doesn't exist, use the year file.
now = datetime.datetime.now()
current_year = str( now.year )
current_month = str( format( now.month, '02' ) )
if os.path.exists( noaa_dir + "NOAA-%s-%s.txt" % ( current_year, current_month ) ):
default_noaa_file = "NOAA-%s-%s.txt" % ( current_year, current_month )
else:
default_noaa_file = "NOAA-%s.txt" % current_year
except:
# There's an error - I've seen this on first run and the NOAA folder is not created yet. Skip this section.
pass
"""
Forecast Data
"""
if self.generator.skin_dict['Extras']['forecast_enabled'] == "1" and self.generator.skin_dict['Extras']['forecast_api_id'] != "" or 'forecast_dev_file' in self.generator.skin_dict['Extras']:
forecast_file = html_root + "/json/forecast.json"
forecast_api_id = self.generator.skin_dict['Extras']['forecast_api_id']
forecast_api_secret = self.generator.skin_dict['Extras']['forecast_api_secret']
forecast_units = self.generator.skin_dict['Extras']['forecast_units'].lower()
latitude = self.generator.config_dict['Station']['latitude']
longitude = self.generator.config_dict['Station']['longitude']
forecast_stale_timer = self.generator.skin_dict['Extras']['forecast_stale']
forecast_is_stale = False
def aeris_coded_weather( data ):
# https://www.aerisweather.com/support/docs/api/reference/weather-codes/
output = ""
coverage_code = data.split(":")[0]
intensity_code = data.split(":")[1]
weather_code = data.split(":")[2]
cloud_dict = {
"CL": label_dict["forecast_cloud_code_CL"],
"FW": label_dict["forecast_cloud_code_FW"],
"SC": label_dict["forecast_cloud_code_SC"],
"BK": label_dict["forecast_cloud_code_BK"],
"OV": label_dict["forecast_cloud_code_OV"]
}
coverage_dict = {
"AR": label_dict["forecast_coverage_code_AR"],
"BR": label_dict["forecast_coverage_code_BR"],
"C": label_dict["forecast_coverage_code_C"],
"D": label_dict["forecast_coverage_code_D"],
"FQ": label_dict["forecast_coverage_code_FQ"],
"IN": label_dict["forecast_coverage_code_IN"],
"IS": label_dict["forecast_coverage_code_IS"],
"L": label_dict["forecast_coverage_code_L"],
"NM": label_dict["forecast_coverage_code_NM"],
"O": label_dict["forecast_coverage_code_O"],
"PA": label_dict["forecast_coverage_code_PA"],
"PD": label_dict["forecast_coverage_code_PD"],
"S": label_dict["forecast_coverage_code_S"],
"SC": label_dict["forecast_coverage_code_SC"],
"VC": label_dict["forecast_coverage_code_VC"],
"WD": label_dict["forecast_coverage_code_WD"]
}
intensity_dict = {
"VL": label_dict["forecast_intensity_code_VL"],
"L": label_dict["forecast_intensity_code_L"],
"H": label_dict["forecast_intensity_code_H"],
"VH": label_dict["forecast_intensity_code_VH"]
}
weather_dict = {
"A": label_dict["forecast_weather_code_A"],
"BD": label_dict["forecast_weather_code_BD"],
"BN": label_dict["forecast_weather_code_BN"],
"BR": label_dict["forecast_weather_code_BR"],
"BS": label_dict["forecast_weather_code_BS"],
"BY": label_dict["forecast_weather_code_BY"],
"F": label_dict["forecast_weather_code_F"],
"FR": label_dict["forecast_weather_code_FR"],
"H": label_dict["forecast_weather_code_H"],
"IC": label_dict["forecast_weather_code_IC"],
"IF": label_dict["forecast_weather_code_IF"],
"IP": label_dict["forecast_weather_code_IP"],
"K": label_dict["forecast_weather_code_K"],
"L": label_dict["forecast_weather_code_L"],
"R": label_dict["forecast_weather_code_R"],
"RW": label_dict["forecast_weather_code_RW"],
"RS": label_dict["forecast_weather_code_RS"],
"SI": label_dict["forecast_weather_code_SI"],
"WM": label_dict["forecast_weather_code_WM"],
"S": label_dict["forecast_weather_code_S"],
"SW": label_dict["forecast_weather_code_SW"],
"T": label_dict["forecast_weather_code_T"],
"UP": label_dict["forecast_weather_code_UP"],
"VA": label_dict["forecast_weather_code_VA"],
"WP": label_dict["forecast_weather_code_WP"],
"ZF": label_dict["forecast_weather_code_ZF"],
"ZL": label_dict["forecast_weather_code_ZL"],
"ZR": label_dict["forecast_weather_code_ZR"],
"ZY": label_dict["forecast_weather_code_ZY"]
}
# Check if the weather_code is in the cloud_dict and use that if it's there. If not then it's a combined weather code.
if weather_code in cloud_dict:
return cloud_dict[weather_code];
else:
# Add the coverage if it's present, and full observation forecast is requested
if coverage_code:
output += coverage_dict[coverage_code] + " "
# Add the intensity if it's present
if intensity_code:
output += intensity_dict[intensity_code] + " "
# Weather output
output += weather_dict[weather_code];
return output
def aeris_icon( data ):
# https://www.aerisweather.com/support/docs/api/reference/icon-list/
icon_name = data.split(".")[0]; # Remove .png
icon_dict = {
"blizzard": "snow",
"blizzardn": "snow",
"blowingsnow": "snow",
"blowingsnown": "snow",
"clear": "clear-day",
"clearn": "clear-night",
"cloudy": "cloudy",
"cloudyn": "cloudy",
"cloudyw": "cloudy",
"cloudywn": "cloudy",
"cold": "clear-day",
"coldn": "clear-night",
"drizzle": "rain",
"drizzlen": "rain",
"dust": "fog",
"dustn": "fog",
"fair": "clear-day",
"fairn": "clear-night",
"drizzlef": "rain",
"fdrizzlen": "rain",
"flurries": "sleet",
"flurriesn": "sleet",
"flurriesw": "sleet",
"flurrieswn": "sleet",
"fog": "fog",
"fogn": "fog",
"freezingrain": "rain",
"freezingrainn": "rain",
"hazy": "fog",
"hazyn": "fog",
"hot": "clear-day",
"N/A ": "unknown",
"mcloudy": "partly-cloudy-day",
"mcloudyn": "partly-cloudy-night",
"mcloudyr": "rain",
"mcloudyrn": "rain",
"mcloudyrw": "rain",
"mcloudyrwn": "rain",
"mcloudys": "snow",
"mcloudysn": "snow",
"mcloudysf": "snow",
"mcloudysfn": "snow",
"mcloudysfw": "snow",
"mcloudysfwn": "snow",
"mcloudysw": "partly-cloudy-day",
"mcloudyswn": "partly-cloudy-night",
"mcloudyt": "thunderstorm",
"mcloudytn": "thunderstorm",
"mcloudytw": "thunderstorm",
"mcloudytwn": "thunderstorm",
"mcloudyw": "partly-cloudy-day",
"mcloudywn": "partly-cloudy-night",
"na": "unknown",
"na": "unknown",
"pcloudy": "partly-cloudy-day",
"pcloudyn": "partly-cloudy-night",
"pcloudyr": "rain",
"pcloudyrn": "rain",
"pcloudyrw": "rain",
"pcloudyrwn": "rain",
"pcloudys": "snow",
"pcloudysn": "snow",
"pcloudysf": "snow",
"pcloudysfn": "snow",
"pcloudysfw": "snow",
"pcloudysfwn": "snow",
"pcloudysw": "partly-cloudy-day",
"pcloudyswn": "partly-cloudy-night",
"pcloudyt": "thunderstorm",
"pcloudytn": "thunderstorm",
"pcloudytw": "thunderstorm",
"pcloudytwn": "thunderstorm",
"pcloudyw": "partly-cloudy-day",
"pcloudywn": "partly-cloudy-night",
"rain": "rain",
"rainn": "rain",
"rainandsnow": "rain",
"rainandsnown": "rain",
"raintosnow": "rain",
"raintosnown": "rain",
"rainw": "rain",
"rainw": "rain",
"showers": "rain",
"showersn": "rain",
"showersw": "rain",
"showersw": "rain",
"sleet": "sleet",
"sleetn": "sleet",
"sleetsnow": "sleet",
"sleetsnown": "sleet",
"smoke": "fog",
"smoken": "fog",
"snow": "snow",
"snown": "snow",
"snoww": "snow",
"snowwn": "snow",
"snowshowers": "snow",
"snowshowersn": "snow",
"snowshowersw": "snow",
"snowshowerswn": "snow",
"snowtorain": "snow",
"snowtorainn": "snow",
"sunny": "clear-day",
"sunnyn": "clear-night",
"sunnyw": "partly-cloudy-day",
"sunnywn": "partly-cloudy-night",
"tstorm": "thunderstorm",
"tstormn": "thunderstorm",
"tstorms": "thunderstorm",
"tstormsn": "thunderstorm",
"tstormsw": "thunderstorm",
"tstormswn": "thunderstorm",
"wind": "wind",
"wind": "wind",
"wintrymix": "sleet",
"wintrymixn": "sleet"
}
return icon_dict[icon_name]
forecast_current_url = "https://api.aerisapi.com/observations/%s,%s?&format=json&filter=allstations&filter=metar&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
forecast_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=day&limit=7&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
if self.generator.skin_dict['Extras']['forecast_alert_enabled'] == "1":
if self.generator.skin_dict['Extras']['forecast_alert_limit']:
forecast_alert_limit = self.generator.skin_dict['Extras']['forecast_alert_limit']
forecast_alerts_url = "https://api.aerisapi.com/alerts/%s,%s?&format=json&limit=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_alert_limit, forecast_api_id, forecast_api_secret )
else:
# Default to 1 alerts to show if the option is missing. Can go up to 10
forecast_alerts_url = "https://api.aerisapi.com/alerts/%s,%s?&format=json&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
# Determine if the file exists and get it's modified time
if os.path.isfile( forecast_file ):
if ( int( time.time() ) - int( os.path.getmtime( forecast_file ) ) ) > int( forecast_stale_timer ):
forecast_is_stale = True
else:
# File doesn't exist, download a new copy
forecast_is_stale = True
# File is stale, download a new copy
if forecast_is_stale:
try:
try:
# Python 3
from urllib.request import Request, urlopen
except ImportError:
# Python 2
from urllib2 import Request, urlopen
user_agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3'
headers = { 'User-Agent' : user_agent }
if 'forecast_dev_file' in self.generator.skin_dict['Extras']:
# Hidden option to use a pre-downloaded forecast file rather than using API calls for no reason
dev_forecast_file = self.generator.skin_dict['Extras']['forecast_dev_file']
req = Request( dev_forecast_file, None, headers )
response = urlopen( req )
forecast_file_result = response.read()
response.close()
else:
# Current conditions
req = Request( forecast_current_url, None, headers )
response = urlopen( req )
current_page = response.read()
response.close()
# Forecast
req = Request( forecast_url, None, headers )
response = urlopen( req )
forecast_page = response.read()
response.close()
if self.generator.skin_dict['Extras']['forecast_alert_enabled'] == "1":
# Alerts
req = Request( forecast_alerts_url, None, headers )
response = urlopen( req )
alerts_page = response.read()
response.close()
# Combine all into 1 file
if self.generator.skin_dict['Extras']['forecast_alert_enabled'] == "1":
try:
forecast_file_result = json.dumps( {"timestamp": int(time.time()), "current": [json.loads(current_page)], "forecast": [json.loads(forecast_page)], "alerts": [json.loads(alerts_page)]} )
except:
forecast_file_result = json.dumps( {"timestamp": int(time.time()), "current": [json.loads(current_page.decode('utf-8'))], "forecast": [json.loads(forecast_page.decode('utf-8'))], "alerts": [json.loads(alerts_page.decode('utf-8'))]} )
else:
try:
forecast_file_result = json.dumps( {"timestamp": int(time.time()), "current": [json.loads(current_page)], "forecast": [json.loads(forecast_page)]} )
except:
forecast_file_result = json.dumps( {"timestamp": int(time.time()), "current": [json.loads(current_page.decode('utf-8'))], "forecast": [json.loads(forecast_page.decode('utf-8'))]} )
except Exception as error:
raise Warning( "Error downloading forecast data. Check the URL in your configuration and try again. You are trying to use URL: %s, and the error is: %s" % ( forecast_url, error ) )
# Save forecast data to file. w+ creates the file if it doesn't exist, and truncates the file and re-writes it everytime
try:
with open( forecast_file, 'wb+' ) as file:
# Python 2/3
try:
file.write( forecast_file_result.encode('utf-8') )
except:
file.write( forecast_file_result )
loginf( "New forecast file downloaded to %s" % forecast_file )
except IOError as e:
raise Warning( "Error writing forecast info to %s. Reason: %s" % ( forecast_file, e) )
# Process the forecast file
with open( forecast_file, "r" ) as read_file:
data = json.load( read_file )
current_obs_summary = aeris_coded_weather( data["current"][0]["response"]["ob"]["weatherPrimaryCoded"] )
current_obs_icon = aeris_icon( data["current"][0]["response"]["ob"]["icon"] ) + ".png"
if forecast_units == "si" or forecast_units == "ca":
if data["current"][0]["response"]["ob"]["visibilityKM"] is not None:
visibility = locale.format("%g", data["current"][0]["response"]["ob"]["visibilityKM"] )
visibility_unit = "km"
else:
visibility = "N/A"
visibility_unit = ""
else:
# us, uk2 and default to miles per hour
if data["current"][0]["response"]["ob"]["visibilityMI"] is not None:
visibility = locale.format("%g", float( data["current"][0]["response"]["ob"]["visibilityMI"] ) )
visibility_unit = "miles"
else:
visibility = "N/A"
visibility_unit = ""
else:
current_obs_icon = ""
current_obs_summary = ""
visibility = "N/A"
visibility_unit = ""
"""
Earthquake Data
"""
# Only process if Earthquake data is enabled
if self.generator.skin_dict['Extras']['earthquake_enabled'] == "1":
earthquake_file = html_root + "/json/earthquake.json"
earthquake_stale_timer = self.generator.skin_dict['Extras']['earthquake_stale']
latitude = self.generator.config_dict['Station']['latitude']
longitude = self.generator.config_dict['Station']['longitude']
earthquake_maxradiuskm = self.generator.skin_dict['Extras']['earthquake_maxradiuskm']
#Sample URL from Belchertown Weather: http://earthquake.usgs.gov/fdsnws/event/1/query?limit=1&lat=42.223&lon=-72.374&maxradiuskm=1000&format=geojson&nodata=204&minmag=2
earthquake_url = "http://earthquake.usgs.gov/fdsnws/event/1/query?limit=1&lat=%s&lon=%s&maxradiuskm=%s&format=geojson&nodata=204&minmag=2" % ( latitude, longitude, earthquake_maxradiuskm )
earthquake_is_stale = False
# Determine if the file exists and get it's modified time
if os.path.isfile( earthquake_file ):
if ( int( time.time() ) - int( os.path.getmtime( earthquake_file ) ) ) > int( earthquake_stale_timer ):
earthquake_is_stale = True
else:
# File doesn't exist, download a new copy
earthquake_is_stale = True
# File is stale, download a new copy
if earthquake_is_stale:
# Download new earthquake data
try:
try:
# Python 3
from urllib.request import Request, urlopen
except ImportError:
# Python 2
from urllib2 import Request, urlopen
user_agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3'
headers = { 'User-Agent' : user_agent }
req = Request( earthquake_url, None, headers )
response = urlopen( req )
page = response.read()
response.close()
if weewx.debug:
logdbg( "Downloading earthquake data using urllib2 was successful" )
except Exception as forecast_error:
if weewx.debug:
logdbg( "Error downloading earthquake data with urllib2, reverting to curl and subprocess. Full error: %s" % forecast_error )
# Nested try - only execute if the urllib2 method fails
try:
import subprocess
command = 'curl -L --silent "%s"' % earthquake_url
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
page = p.communicate()[0]
if weewx.debug:
logdbg( "Downloading earthquake data with curl was successful." )
except Exception as error:
raise Warning( "Error downloading earthquake data using urllib2 and subprocess curl. Your software may need to be updated, or the URL is incorrect. You are trying to use URL: %s, and the error is: %s" % ( earthquake_url, error ) )
# Save earthquake data to file. w+ creates the file if it doesn't exist, and truncates the file and re-writes it everytime
try:
with open( earthquake_file, 'wb+' ) as file:
file.write( page )
if weewx.debug:
logdbg( "Earthquake data saved to %s" % earthquake_file )
except IOError as e:
raise Warning( "Error writing earthquake data to %s. Reason: %s" % ( earthquake_file, e) )
# Process the earthquake file
with open( earthquake_file, "r" ) as read_file:
try:
eqdata = json.load( read_file )
except:
eqdata = ""
try:
eqtime = eqdata["features"][0]["properties"]["time"] / 1000
equrl = eqdata["features"][0]["properties"]["url"]
eqplace = eqdata["features"][0]["properties"]["place"]
eqmag = eqdata["features"][0]["properties"]["mag"]
eqlat = str( round( eqdata["features"][0]["geometry"]["coordinates"][0], 4 ) )
eqlon = str( round( eqdata["features"][0]["geometry"]["coordinates"][1], 4 ) )
except:
# No earthquake data
eqtime = label_dict["earthquake_no_data"]
equrl = ""
eqplace = ""
eqmag = ""
eqlat = ""
eqlon = ""
else:
eqtime = ""
equrl = ""
eqplace = ""
eqmag = ""
eqlat = ""
eqlon = ""
"""
Version Update Data
"""
if self.generator.skin_dict['Extras']['check_for_updates'] == "1":
github_version_file = html_root + "/json/github_version.json"
github_version_is_stale = False
github_version_url = "https://api.github.com/repos/poblabs/weewx-belchertown/releases/latest"
# Determine if the file exists and get it's modified time. If it's older than an hour then it's stale
if os.path.isfile( github_version_file ):
if ( int( time.time() ) - int( os.path.getmtime( github_version_file ) ) ) > 21600:
github_version_is_stale = True
else:
# File doesn't exist, download a new copy
github_version_is_stale = True
# File is stale, download a new copy
if github_version_is_stale:
# Download new GitHub data
try:
try:
# Python 3
from urllib.request import Request, urlopen
except ImportError:
# Python 2
from urllib2 import Request, urlopen
user_agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3'
headers = { 'User-Agent' : user_agent }
req = Request( github_version_url, None, headers )
response = urlopen( req )
#page = response.read()
page = json.load( response )
response.close()
except Exception as error:
logerr( "Update Checker: Error downloading GitHub Version data. The error is: %s" % error )
try:
# Only save the tag_name. Typical tag is weewx-belchertown-x.y where x.y is the version number. So split on "-" and save the version number only
tag_name = page["tag_name"].split("-")[2]
try:
# Save data to file. w+ creates the file if it doesn't exist, and truncates the file and re-writes it everytime
with open( github_version_file, 'wb+' ) as file:
file.write( tag_name )
loginf( "Update Checker: New GitHub Version file downloaded to %s" % github_version_file )
except IOError as e:
logerr( "Update Checker: Error writing GitHub Version info to %s. Reason: %s" % ( github_version_file, e) )
except:
pass
try:
# Process the file
with open( github_version_file, "r" ) as read_file:
data = read_file.read()
except IOError as e:
logerr( "Update Checker: Unable to open %s. Reason: %s" % ( github_version_file, e) )
data = ""
github_version = data
else:
# Empty default
github_version = ""
"""
Get Current Station Observation Data for the table html
"""
station_obs_binding = None
station_obs_json = OrderedDict()
station_obs_html = ""
station_observations = self.generator.skin_dict['Extras']['station_observations']
# Check if this is a list. If not then we have 1 item, so force it into a list
if isinstance(station_observations, list) is False:
station_observations = station_observations.split()
current_stamp = manager.lastGoodStamp()
current = weewx.tags.CurrentObj(db_lookup, station_obs_binding, current_stamp, self.generator.formatter, self.generator.converter)
for obs in station_observations:
if "data_binding" in obs:
station_obs_binding = obs[obs.find("(")+1:obs.rfind(")")].split("=")[1] # Thanks https://stackoverflow.com/a/40811994/1177153
obs = obs.split("(")[0]
if station_obs_binding is not None:
obs_binding_manager = self.generator.db_binder.get_manager(station_obs_binding)
current_stamp = obs_binding_manager.lastGoodStamp()
current = weewx.tags.CurrentObj(db_lookup, station_obs_binding, current_stamp, self.generator.formatter, self.generator.converter)
if obs == "visibility":
try:
obs_output = str(visibility) + " " + str(visibility_unit)
except:
raise Warning( "Error adding visiblity to station observations table. Check that you have forecast data, or remove visibility from your station_observations Extras option." )
elif obs == "rainWithRainRate":
# rainWithRainRate Rain shows rain daily sum and rain rate
obs_binder = weewx.tags.ObservationBinder("rain", archiveDaySpan(current_stamp), db_lookup, None, "day", self.generator.formatter, self.generator.converter)
dayRain_sum = getattr(obs_binder, "sum")
# Need to use dayRain for class name since that is weewx-mqtt payload's name
obs_rain_output = "<span class='dayRain'>%s</span><!-- AJAX -->" % str(dayRain_sum)
obs_rain_output += " <span class='border-left'> </span>"
obs_rain_output += "<span class='rainRate'>%s</span><!-- AJAX -->" % str(getattr(current, "rainRate"))
# Empty field for the JSON "current" output
obs_output = ""
else:
obs_output = getattr(current, obs)
if "?" in str(obs_output):
# Try to catch those invalid observations, like 'uv' needs to be 'UV'.
obs_output = "Invalid observation"
# Build the json "current" array for weewx_data.json for JavaScript
if obs not in station_obs_json:
station_obs_json[obs] = str(obs_output)
# Build the HTML for the front page
station_obs_html += "<tr>"
station_obs_html += "<td class='station-observations-label'>%s</td>" % label_dict[obs]
station_obs_html += "<td>"
if obs == "rainWithRainRate":
# Add special rain + rainRate one liner
station_obs_html += obs_rain_output
else:
station_obs_html += "<span class=%s>%s</span><!-- AJAX -->" % ( obs, obs_output )
if obs == "barometer" or obs == "pressure" or obs == "altimeter":
# Append the trend arrow to the pressure observation. Need this for non-mqtt pages
trend = weewx.tags.TrendObj(10800, 300, db_lookup, None, current_stamp, self.generator.formatter, self.generator.converter)
obs_trend = getattr(trend, obs)
station_obs_html += ' <span class="pressure-trend">' # Maintain leading spacing
if str(obs_trend) == "N/A":
pass
elif "-" in str(obs_trend):
station_obs_html += '<i class="fa fa-arrow-down barometer-down"></i>'
else:
station_obs_html += '<i class="fa fa-arrow-up barometer-up"></i>'
station_obs_html += '</span>' # Close the span
station_obs_html += "</td>"
station_obs_html += "</tr>"
"""
Get all observations and their rounding values
"""
all_obs_rounding_json = OrderedDict()
all_obs_unit_labels_json = OrderedDict()
for obs in sorted(weewx.units.obs_group_dict):
try:
# Find the unit from group (like group_temperature = degree_F)
obs_group = weewx.units.obs_group_dict[obs]
obs_unit = self.generator.converter.group_unit_dict[obs_group]
except:
# Something's wrong. Continue this loop to ignore this group (like group_dust or something non-standard)
continue
try:
# Find the number of decimals to round to based on group name
obs_round = self.generator.skin_dict['Units']['StringFormats'].get(obs_unit, "0")[2]
except:
obs_round = self.generator.skin_dict['Units']['StringFormats'].get(obs_unit, "0")
# Add to the rounding array
if obs not in all_obs_rounding_json:
all_obs_rounding_json[obs] = str(obs_round)
# Get the unit's label
obs_unit_label = self.generator.skin_dict['Units']['Labels'].get(obs_unit, "")
# Add to label array and strip whitespace if possible
if obs not in all_obs_unit_labels_json:
all_obs_unit_labels_json[obs] = obs_unit_label
# Special handling items
if visibility:
all_obs_rounding_json["visibility"] = "2"
all_obs_unit_labels_json["visibility"] = visibility_unit
else:
all_obs_rounding_json["visibility"] = ""
all_obs_unit_labels_json["visibility"] = ""
"""
Social Share
"""
facebook_enabled = self.generator.skin_dict['Extras']['facebook_enabled']
twitter_enabled = self.generator.skin_dict['Extras']['twitter_enabled']
social_share_html = self.generator.skin_dict['Extras']['social_share_html']
twitter_text = label_dict["twitter_text"]
twitter_owner = label_dict["twitter_owner"]
twitter_hashtags = label_dict["twitter_hashtags"]
if facebook_enabled == "1":
facebook_html = """
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<div class="fb-like" data-href="%s" data-width="500px" data-layout="button_count" data-action="like" data-show-faces="false" data-share="true"></div>
""" % social_share_html
else:
facebook_html = ""
if twitter_enabled == "1":
twitter_html = """
<script>
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
</script>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="%s" data-text="%s" data-via="%s" data-hashtags="%s">Tweet</a>
""" % ( social_share_html, twitter_text, twitter_owner, twitter_hashtags )
else:
twitter_html = ""
# Build the output
social_html = ""
if facebook_html != "" or twitter_html != "":
social_html = '<div class="wx-stn-share">'
# Facebook first
if facebook_html != "":
social_html += facebook_html
# Add a separator margin if both are enabled
if facebook_html != "" and twitter_html != "":
social_html += '<div class="wx-share-sep"></div>'
# Twitter second
if twitter_html != "":
social_html += twitter_html
social_html += "</div>"
"""
Include custom.css if it exists in the HTML_ROOT folder
"""
custom_css_file = html_root + "/custom.css"
# Determine if the file exists
if os.path.isfile( custom_css_file ):
custom_css_exists = True
else:
custom_css_exists = False
# Build the search list with the new values
search_list_extension = { 'belchertown_version': VERSION,
'belchertown_debug': belchertown_debug,
'moment_js_utc_offset': moment_js_utc_offset,
'highcharts_timezoneoffset': highcharts_timezoneoffset,
'system_locale': system_locale,
'system_locale_js': system_locale_js,
'locale_encoding': locale_encoding,
'highcharts_decimal': highcharts_decimal,
'highcharts_thousands': highcharts_thousands,
'radar_html': radar_html,
'archive_interval_ms': archive_interval_ms,
'ordinate_names': ordinate_names,
'charts': json.dumps(charts),
'graphpage_titles': json.dumps(graphpage_titles),
'graphpage_titles_dict': graphpage_titles,
'graphpage_content': json.dumps(graphpage_content),
'graph_page_buttons': graph_page_buttons,
'alltime' : all_stats,
'year_outTemp_range_max': year_outTemp_range_max,
'year_outTemp_range_min': year_outTemp_range_min,
'at_outTemp_range_max' : at_outTemp_range_max,
'at_outTemp_range_min': at_outTemp_range_min,
'rainiest_day': rainiest_day,
'at_rainiest_day': at_rainiest_day,
'year_rainiest_month': year_rainiest_month,
'at_rainiest_month': at_rainiest_month,
'at_rain_highest_year': at_rain_highest_year,
'year_days_with_rain': year_days_with_rain,
'year_days_without_rain': year_days_without_rain,
'at_days_with_rain': at_days_with_rain,
'at_days_without_rain': at_days_without_rain,
'windSpeedUnitLabel': windSpeed_unit_label,
'noaa_header_html': noaa_header_html,
'default_noaa_file': default_noaa_file,
'current_obs_icon': current_obs_icon,
'current_obs_summary': current_obs_summary,
'visibility': visibility,
'visibility_unit': visibility_unit,
'station_obs_json': json.dumps(station_obs_json),
'station_obs_html': station_obs_html,
'all_obs_rounding_json': json.dumps(all_obs_rounding_json),
'all_obs_unit_labels_json': json.dumps(all_obs_unit_labels_json),
'earthquake_time': eqtime,
'earthquake_url': equrl,
'earthquake_place': eqplace,
'earthquake_magnitude': eqmag,
'earthquake_lat': eqlat,
'earthquake_lon': eqlon,
'github_version': github_version,
'social_html': social_html,
'custom_css_exists': custom_css_exists }
# Finally, return our extension as a list:
return [search_list_extension]
# =============================================================================
# HighchartsJsonGenerator
# =============================================================================
class HighchartsJsonGenerator(weewx.reportengine.ReportGenerator):
"""Class for generating JSON files for the Highcharts.
Adapted from the ImageGenerator class.
Useful attributes (some inherited from ReportGenerator):
config_dict: The weewx configuration dictionary
skin_dict: The dictionary for this skin
gen_ts: The generation time
first_run: Is this the first time the generator has been run?
stn_info: An instance of weewx.station.StationInfo
record: A copy of the "current" record. May be None.
formatter: An instance of weewx.units.Formatter
converter: An instance of weewx.units.Converter
search_list_objs: A list holding search list extensions
db_binder: An instance of weewx.manager.DBBinder from which the
data should be extracted
"""
def run(self):
"""Main entry point for file generation."""
chart_config_path = os.path.join(
self.config_dict['WEEWX_ROOT'],
self.skin_dict['SKIN_ROOT'],
self.skin_dict.get('skin', ''),
'graphs.conf')
default_chart_config_path = os.path.join(
self.config_dict['WEEWX_ROOT'],
self.skin_dict['SKIN_ROOT'],
self.skin_dict.get('skin', ''),
'graphs.conf.example')
if os.path.exists( chart_config_path ):
self.chart_dict = configobj.ConfigObj(chart_config_path, file_error=True)
else:
self.chart_dict = configobj.ConfigObj(default_chart_config_path, file_error=True)
self.converter = weewx.units.Converter.fromSkinDict(self.skin_dict)
self.formatter = weewx.units.Formatter.fromSkinDict(self.skin_dict)
# Setup title dict for plot titles
try:
d = self.skin_dict['Labels']['Generic']
except KeyError:
d = {}
label_dict = weeutil.weeutil.KeyDict(d)
# Final output dict
output = {}
# Loop through each [section]. This is the first bracket group of options including global options.
for chart_group in self.chart_dict.sections:
output[chart_group] = OrderedDict() # This retains the order in which to load the charts on the page.
chart_options = accumulateLeaves(self.chart_dict[chart_group])
output[chart_group]["belchertown_version"] = VERSION
output[chart_group]["generated_timestamp"] = time.strftime('%m/%d/%Y %H:%M:%S')
# Setup the JSON file name for each chart group
html_dest_dir = os.path.join(self.config_dict['WEEWX_ROOT'],
self.skin_dict['HTML_ROOT'],
"json")
json_filename = html_dest_dir + "/" + chart_group + ".json"
# Default back to Highcharts standards
colors = chart_options.get("colors", "#7cb5ec, #b2df8a, #f7a35c, #8c6bb1, #dd3497, #e4d354, #268bd2, #f45b5b, #6a3d9a, #33a02c")
output[chart_group]["colors"] = colors
# chartgroup_title is used on the graphs page
chartgroup_title = chart_options.get('title', None)
if chartgroup_title:
output[chart_group]["chartgroup_title"] = chartgroup_title
# Define the default tooltip datetime format from the global options
tooltip_date_format = chart_options.get('tooltip_date_format', "LLLL")
output[chart_group]["tooltip_date_format"] = tooltip_date_format
# Credits Text
credits = chart_options.get("credits", "highcharts_default")
output[chart_group]["credits"] = credits
# Credits URL
credits_url = chart_options.get("credits_url", "highcharts_default")
output[chart_group]["credits_url"] = credits_url
# Credits position
credits_position = chart_options.get("credits_position", "highcharts_default")
output[chart_group]["credits_position"] = credits_position
# Check if there are any user override on generation periods.
# Takes the crontab approach. If the words hourly, daily, monthly, yearly are present use them, otherwise use an integer interval if available.
# Since weewx could be restarted, we'll lose our end-timestamp to trigger off of for chart staleness.
# So we have to use the timestamp of the file to generate this. If the file does not exist, we need to create it first.
# Once created we use that to see if we need to generate a fresh data set for the chart.
generate = chart_options.get('generate', None)
if generate is not None:
# Default to not making a new chart
create_new_chart = False
# Get our intervals. Minus 60 seconds so that it'll run a little more reliably on the next interval.
if generate.lower() == "hourly":
chart_stale_timer = 3540
elif generate.lower() == "daily":
chart_stale_timer = 86340
elif generate.lower() == "weekly":
chart_stale_timer = 604740
elif generate.lower() == "monthly":
chart_stale_timer = 2629686
elif generate.lower() == "yearly":
chart_stale_timer = 31556892
else:
chart_stale_timer = int(generate)
if not os.path.isfile(json_filename):
# File doesn't exist. Chart is stale no matter what.
create_new_chart = True
else:
# The file exists get timestamp to compare against what the user wants for an interval
if ( int( time.time() ) - int( os.path.getmtime( json_filename ) ) ) >= int( chart_stale_timer ):
create_new_chart = True
# Chart isn't stale, so continue to next chart (this current chart_group is skipped and not generated)
if not create_new_chart:
continue
# Loop through each [[chart_group]] within the section.
for plotname in self.chart_dict[chart_group].sections:
output[chart_group][plotname] = {}
output[chart_group][plotname]["series"] = OrderedDict() # This retains the observation position in the dictionary to match the order in the conf so the chart is in the right user-defined order
output[chart_group][plotname]["options"] = {}
#output[chart_group][plotname]["options"]["renderTo"] = chart_group + plotname # daychart1, weekchart1, etc. Used for the graphs page and the different chart_groups
output[chart_group][plotname]["options"]["renderTo"] = plotname # daychart1, weekchart1, etc. Used for the graphs page and the different chart_groups
output[chart_group][plotname]["options"]["chart_group"] = chart_group
plot_options = accumulateLeaves(self.chart_dict[chart_group][plotname])
# Setup the database binding, default to weewx.conf's binding if none supplied.
binding = plot_options.get('data_binding', self.config_dict['StdReport'].get('data_binding', 'wx_binding'))
archive = self.db_binder.get_manager(binding)
#Generate timespan for the string time windows
start_ts = archive.firstGoodStamp()
stop_ts = archive.lastGoodStamp()
timespan = weeutil.weeutil.TimeSpan(start_ts, stop_ts)
# Find timestamps for the rolling window
plotgen_ts = self.gen_ts
if not plotgen_ts:
plotgen_ts = stop_ts
if not plotgen_ts:
plotgen_ts = time.time()
chart_title = plot_options.get("title", "")
output[chart_group][plotname]["options"]["title"] = chart_title
chart_subtitle = plot_options.get("subtitle", "")
output[chart_group][plotname]["options"]["subtitle"] = chart_subtitle
# Get the type of plot ("bar', 'line', 'spline', or 'scatter')
plottype = plot_options.get('type', 'line')
output[chart_group][plotname]["options"]["type"] = plottype
gapsize = plot_options.get('gapsize', 300000) # Default to 5 minutes in millis
if gapsize:
output[chart_group][plotname]["options"]["gapsize"] = gapsize
connectNulls = plot_options.get("connectNulls", "false")
output[chart_group][plotname]["options"]["connectNulls"] = connectNulls
xAxis_groupby = plot_options.get('xAxis_groupby', None)
xAxis_categories = plot_options.get('xAxis_categories', "")
# Check if this is a list. If not then we have 1 item, so force it into a list
if isinstance(xAxis_categories, list) is False:
xAxis_categories = xAxis_categories.split()
output[chart_group][plotname]["options"]["xAxis_categories"] = xAxis_categories
# Grab any per-chart tooltip date format overrides
plot_tooltip_date_format = plot_options.get('tooltip_date_format', None)
output[chart_group][plotname]["options"]["plot_tooltip_date_format"] = plot_tooltip_date_format
# Width and height specific CSS overrides
output[chart_group][plotname]["options"]["css_width"] = plot_options.get('width', "")
output[chart_group][plotname]["options"]["css_height"] = plot_options.get('height', "")
# Setup exporting option
exporting = plot_options.get('exporting', None)
if exporting is not None and to_bool(exporting):
# Only turn on exporting if it's not none and it's true (1 or True)
output[chart_group][plotname]["options"]["exporting"] = "true"
else:
output[chart_group][plotname]["options"]["exporting"] = "false"
# Loop through each [[[observation]]] within the chart_group.
for line_name in self.chart_dict[chart_group][plotname].sections:
output[chart_group][plotname]["series"][line_name] = {}
output[chart_group][plotname]["series"][line_name]["obsType"] = line_name
line_options = accumulateLeaves(self.chart_dict[chart_group][plotname][line_name])
# Look for any keyword timespans first and default to those start/stop times for the chart
time_length = line_options.get('time_length', 86400)
time_ago = int(line_options.get('time_ago', 1))
day_specific = line_options.get('day_specific', 1) # Force a day so we don't error out
month_specific = line_options.get('month_specific', 8) # Force a month so we don't error out
year_specific = line_options.get('year_specific', 2019) # Force a year so we don't error out
if time_length == "today":
minstamp, maxstamp = archiveDaySpan( timespan.stop )
elif time_length == "week":
week_start = to_int(self.config_dict["Station"].get('week_start', 6))
minstamp, maxstamp = archiveWeekSpan( timespan.stop, week_start )
elif time_length == "month":
minstamp, maxstamp = archiveMonthSpan( timespan.stop )
elif time_length == "year":
minstamp, maxstamp = archiveYearSpan( timespan.stop )
elif time_length == "days_ago":
minstamp, maxstamp = archiveDaySpan( timespan.stop, days_ago=time_ago )
elif time_length == "weeks_ago":
week_start = to_int(self.config_dict["Station"].get('week_start', 6))
minstamp, maxstamp = archiveWeekSpan( timespan.stop, week_start, weeks_ago=time_ago )
elif time_length == "months_ago":
minstamp, maxstamp = archiveMonthSpan( timespan.stop, months_ago=time_ago )
elif time_length == "years_ago":
minstamp, maxstamp = archiveYearSpan( timespan.stop, years_ago=time_ago )
elif time_length == "day_specific":
# Set an arbitrary hour within the specific day to get that full day timespan and not the day before. e.g. 1pm
day_dt = datetime.datetime.strptime(str(year_specific) + '-' + str(month_specific) + '-' + str(day_specific) + ' 13', '%Y-%m-%d %H')
daystamp = int(time.mktime(day_dt.timetuple()))
minstamp, maxstamp = archiveDaySpan( daystamp )
elif time_length == "month_specific":
# Set an arbitrary day within the specific month to get that full month timespan and not the day before. e.g. 5th day
month_dt = datetime.datetime.strptime(str(year_specific) + '-' + str(month_specific) + '-5', '%Y-%m-%d')
monthstamp = int(time.mktime(month_dt.timetuple()))
minstamp, maxstamp = archiveMonthSpan( monthstamp )
elif time_length == "year_specific":
# Get a date in the middle of the year to get the full year epoch so weewx can find the year timespan.
year_dt = datetime.datetime.strptime(str(year_specific) + '-8-1', '%Y-%m-%d')
yearstamp = int(time.mktime(year_dt.timetuple()))
minstamp, maxstamp = archiveYearSpan( yearstamp )
elif time_length == "timespan_specific":
minstamp = line_options.get('timespan_start', None)
maxstamp = line_options.get('timespan_stop', None)
if minstamp is None or maxstamp is None:
raise Warning( "Error trying to create timespan_specific graph. You are missing either timespan_start or timespan_stop options." )
elif time_length == "all":
minstamp = start_ts
maxstamp = stop_ts
else:
# Rolling timespans using seconds
time_length = int(time_length) # Convert to int() for minstamp math and for point_timestamp conditional later
minstamp = plotgen_ts - time_length # Take the generation time and subtract the time_length to get our start time
maxstamp = plotgen_ts
# Find if this chart is using a new database binding. Default to the binding set in plot_options
binding = line_options.get('data_binding', binding)
archive = self.db_binder.get_manager(binding)
# Find the observation type if specified (e.g. more than 1 of the same on a chart). (e.g. outTemp, rainFall, windDir, etc.)
observation_type = line_options.get('observation_type', line_name)
# If we have a weather range, define what the actual observation type to lookup in the db is, and to use for yAxis labels
weatherRange_obs_lookup = line_options.get('range_type', None)
# Get any custom names for this observation
name = line_options.get('name', None)
if not name:
# No explicit name. Look up a generic one. NB: label_dict is a KeyDict which
# will substitute the key if the value is not in the dictionary.
if weatherRange_obs_lookup is not None:
name = label_dict[weatherRange_obs_lookup]
else:
name = label_dict[observation_type]
# Get the unit label
if observation_type == "rainTotal":
obs_label = "rain"
elif observation_type == "weatherRange" and weatherRange_obs_lookup is not None:
obs_label = weatherRange_obs_lookup
else:
obs_label = observation_type
unit_label = line_options.get('yAxis_label_unit', weewx.units.get_label_string(self.formatter, self.converter, obs_label))
# Set the yAxis label. Place into series for custom JavaScript. Highcharts will ignore these by default
yAxisLabel_config = line_options.get('yAxis_label', None)
# Set a default yAxis label if graphs.conf yAxis_label is none and there's a unit_label - e.g. Temperature (F)
if yAxisLabel_config is None and unit_label:
# Python 2/3 hack
try:
yAxis_label = name + " (" + unit_label.strip().encode("utf-8") + ")" # Python 2.
except:
yAxis_label = name + " (" + unit_label.strip() + ")" # Python 3
elif yAxisLabel_config:
yAxis_label = yAxisLabel_config
else:
# Unknown observation, set the default label to ""
yAxis_label = ""
output[chart_group][plotname]["options"]["yAxis_label"] = yAxis_label
output[chart_group][plotname]["series"][line_name]["yAxis_label"] = yAxis_label
# Look for aggregation type:
aggregate_type = line_options.get('aggregate_type')
if aggregate_type in (None, '', 'None', 'none'):
# No aggregation specified.
aggregate_type = aggregate_interval = None
else:
try:
# Aggregation specified. Get the interval.
aggregate_interval = line_options.as_int('aggregate_interval')
except KeyError:
syslog.syslog(syslog.LOG_ERR, "HighchartsJsonGenerator: aggregate interval required for aggregate type %s" % aggregate_type)
syslog.syslog(syslog.LOG_ERR, "HighchartsJsonGenerator: line type %s skipped" % observation_type)
continue
# Mirrored charts
mirrored_value = line_options.get('mirrored_value', None)
# Custom CSS
css_class = line_options.get('css_class', None)
output[chart_group][plotname]["options"]["css_class"] = css_class
# Setup polar charts
polar = line_options.get('polar', None)
if polar is not None and to_bool(polar):
# Only turn on polar if it's not none and it's true (1 or True)
output[chart_group][plotname]["series"][line_name]["polar"] = "true"
else:
output[chart_group][plotname]["series"][line_name]["polar"] = "false"
# This for loop is to get any user provided highcharts series config data. Built-in highcharts variable names accepted.
for highcharts_config, highcharts_value in self.chart_dict[chart_group][plotname][line_name].items():
output[chart_group][plotname]["series"][line_name][highcharts_config] = highcharts_value
# Override any highcharts series configs with standardized data, then generate the data output
output[chart_group][plotname]["series"][line_name]["name"] = name
# Set the yAxis min and max if present. Useful for the rxCheckPercent plots
yAxis_min = line_options.get('yAxis_min', None)
if yAxis_min:
output[chart_group][plotname]["series"][line_name]["yAxis_min"] = yAxis_min
yAxis_max = line_options.get('yAxis_max', None)
if yAxis_max:
output[chart_group][plotname]["series"][line_name]["yAxis_max"] = yAxis_max
# Add rounding from weewx.conf/skin.conf so Highcharts can use it
if observation_type == "rainTotal":
rounding_obs_lookup = "rain"
else:
rounding_obs_lookup = observation_type
try:
obs_group = weewx.units.obs_group_dict[rounding_obs_lookup]
obs_unit = self.converter.group_unit_dict[obs_group]
obs_round = self.skin_dict['Units']['StringFormats'].get(obs_unit, "0")[2]
output[chart_group][plotname]["series"][line_name]["rounding"] = obs_round
except:
# Not a valid weewx schema name - maybe this is windRose or something?
output[chart_group][plotname]["series"][line_name]["rounding"] = "-1"
# Build series data
series_data = self.get_observation_data(binding, archive, observation_type, minstamp, maxstamp, aggregate_type, aggregate_interval, time_length, xAxis_groupby, xAxis_categories, mirrored_value, weatherRange_obs_lookup)
# Build the final series data JSON
if isinstance(series_data, dict):
# If the returned type is a dict, then it's from the xAxis groupby section containing labels. Need to repack data, and update xAxis_categories.
# Use SQL Labels?
if "use_sql_labels" in series_data:
if series_data["use_sql_labels"]:
output[chart_group][plotname]["options"]["xAxis_categories"] = series_data["xAxis_groupby_labels"]
elif "weatherRange" in series_data:
output[chart_group][plotname]["series"][line_name]["range_unit"] = series_data["range_unit"]
output[chart_group][plotname]["series"][line_name]["range_unit_label"] = series_data["range_unit_label"]
# No matter what, reset data back to just the series data and not a dict of values
output[chart_group][plotname]["series"][line_name]["data"] = list(series_data["obsdata"])
else:
# No custom series data overrides, so just add series_data to the chart series data
output[chart_group][plotname]["series"][line_name]["data"] = list(series_data)
# Final pass through self.highcharts_series_options_to_float() to convert the remaining options with numeric values to float
# such that Highcharts can make use of them.
output[chart_group][plotname]["series"][line_name] = self.highcharts_series_options_to_float(output[chart_group][plotname]["series"][line_name])
# Write the output to the JSON file
with open(json_filename, mode='w') as jf:
jf.write( json.dumps( output[chart_group] ) )
# Save the graphs.conf to a json file for future debugging
chart_json_filename = html_dest_dir + "/graphs.json"
with open(chart_json_filename, mode='w') as cjf:
cjf.write( json.dumps( self.chart_dict ) )
def get_observation_data(self, binding, archive, observation, start_ts, end_ts, aggregate_type, aggregate_interval, time_length, xAxis_groupby, xAxis_categories, mirrored_value, weatherRange_obs_lookup):
"""Get the SQL vectors for the observation, the aggregate type and the interval of time"""
if observation == "windRose":
# Special Belchertown wind rose with Highcharts aggregator
# Wind speeds are split into the first 7 beaufort groups. https://en.wikipedia.org/wiki/Beaufort_scale
# Force no aggregate_type
if aggregate_type:
aggregate_type = None
# Force no aggregate_interval
if aggregate_interval:
aggregate_interval = None
# Get windDir observations.
obs_lookup = "windDir"
(time_start_vt, time_stop_vt, windDir_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
#windDir_vt = self.converter.convert(windDir_vt)
#usage_round = int(self.skin_dict['Units']['StringFormats'].get(windDir_vt[2], "0f")[-2])
usage_round = 0 # Force round to 0 decimal
windDir_round_vt = [self.round_none(x, usage_round) for x in windDir_vt[0]]
#windDir_round_vt = [0.0 if v is None else v for v in windDir_round_vt]
# Get windSpeed observations.
obs_lookup = "windSpeed"
(time_start_vt, time_stop_vt, windSpeed_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
windSpeed_vt = self.converter.convert(windSpeed_vt)
usage_round = int(self.skin_dict['Units']['StringFormats'].get(windSpeed_vt[2], "2f")[-2])
windSpeed_round_vt = [self.round_none(x, usage_round) for x in windSpeed_vt[0]]
# Exit if the vectors are None
if windDir_vt[1] is None or windSpeed_vt[1] is None:
empty_windrose = [{ "name": "",
"data": []
}]
return empty_windrose
# Get the unit label from the skin dict for speed.
windSpeed_unit = windSpeed_vt[1]
windSpeed_unit_label = self.skin_dict["Units"]["Labels"][windSpeed_unit]
# Merge the two outputs so we have a consistent data set to filter on
merged = zip(windDir_round_vt, windSpeed_round_vt)
# Sort by beaufort wind speeds
group_0_windDir, group_0_windSpeed, group_1_windDir, group_1_windSpeed, group_2_windDir, group_2_windSpeed, group_3_windDir, group_3_windSpeed, group_4_windDir, group_4_windSpeed, group_5_windDir, group_5_windSpeed, group_6_windDir, group_6_windSpeed = ([] for i in range(14))
for windData in merged:
if windData[0] is not None and windData[1] is not None:
if windSpeed_unit == "mile_per_hour" or windSpeed_unit == "mile_per_hour2":
if windData[1] < 1:
group_0_windDir.append( windData[0] )
group_0_windSpeed.append( windData[1] )
elif 1 <= windData[1] <= 3:
group_1_windDir.append( windData[0] )
group_1_windSpeed.append( windData[1] )
elif 4 <= windData[1] <= 7:
group_2_windDir.append( windData[0] )
group_2_windSpeed.append( windData[1] )
elif 8 <= windData[1] <= 12:
group_3_windDir.append( windData[0] )
group_3_windSpeed.append( windData[1] )
elif 13 <= windData[1] <= 18:
group_4_windDir.append( windData[0] )
group_4_windSpeed.append( windData[1] )
elif 19 <= windData[1] <= 24:
group_5_windDir.append( windData[0] )
group_5_windSpeed.append( windData[1] )
elif windData[1] >= 25:
group_6_windDir.append( windData[0] )
group_6_windSpeed.append( windData[1] )
elif windSpeed_unit == "km_per_hour" or windSpeed_unit == "km_per_hour2":
if windData[1] < 2:
group_0_windDir.append( windData[0] )
group_0_windSpeed.append( windData[1] )
elif 2 <= windData[1] <= 5:
group_1_windDir.append( windData[0] )
group_1_windSpeed.append( windData[1] )
elif 6 <= windData[1] <= 11:
group_2_windDir.append( windData[0] )
group_2_windSpeed.append( windData[1] )
elif 12 <= windData[1] <= 19:
group_3_windDir.append( windData[0] )
group_3_windSpeed.append( windData[1] )
elif 20 <= windData[1] <= 28:
group_4_windDir.append( windData[0] )
group_4_windSpeed.append( windData[1] )
elif 29 <= windData[1] <= 38:
group_5_windDir.append( windData[0] )
group_5_windSpeed.append( windData[1] )
elif windData[1] >= 39:
group_6_windDir.append( windData[0] )
group_6_windSpeed.append( windData[1] )
elif windSpeed_unit == "meter_per_second" or windSpeed_unit == "meter_per_second2":
if windData[1] < 0.5:
group_0_windDir.append( windData[0] )
group_0_windSpeed.append( windData[1] )
elif 0.5 <= windData[1] <= 1.5:
group_1_windDir.append( windData[0] )
group_1_windSpeed.append( windData[1] )
elif 1.6 <= windData[1] <= 3.3:
group_2_windDir.append( windData[0] )
group_2_windSpeed.append( windData[1] )
elif 3.4 <= windData[1] <= 5.5:
group_3_windDir.append( windData[0] )
group_3_windSpeed.append( windData[1] )
elif 5.6 <= windData[1] <= 7.9:
group_4_windDir.append( windData[0] )
group_4_windSpeed.append( windData[1] )
elif 8 <= windData[1] <= 10.7:
group_5_windDir.append( windData[0] )
group_5_windSpeed.append( windData[1] )
elif windData[1] >= 10.8:
group_6_windDir.append( windData[0] )
group_6_windSpeed.append( windData[1] )
elif windSpeed_unit == "knot" or windSpeed_unit == "knot2":
if windData[1] < 1:
group_0_windDir.append( windData[0] )
group_0_windSpeed.append( windData[1] )
elif 1 <= windData[1] <= 3:
group_1_windDir.append( windData[0] )
group_1_windSpeed.append( windData[1] )
elif 4 <= windData[1] <= 6:
group_2_windDir.append( windData[0] )
group_2_windSpeed.append( windData[1] )
elif 7 <= windData[1] <= 10:
group_3_windDir.append( windData[0] )
group_3_windSpeed.append( windData[1] )
elif 11 <= windData[1] <= 16:
group_4_windDir.append( windData[0] )
group_4_windSpeed.append( windData[1] )
elif 17 <= windData[1] <= 21:
group_5_windDir.append( windData[0] )
group_5_windSpeed.append( windData[1] )
elif windData[1] >= 22:
group_6_windDir.append( windData[0] )
group_6_windSpeed.append( windData[1] )
# Get the windRose data
group_0_series_data = self.create_windrose_data( group_0_windDir, group_0_windSpeed )
group_1_series_data = self.create_windrose_data( group_1_windDir, group_1_windSpeed )
group_2_series_data = self.create_windrose_data( group_2_windDir, group_2_windSpeed )
group_3_series_data = self.create_windrose_data( group_3_windDir, group_3_windSpeed )
group_4_series_data = self.create_windrose_data( group_4_windDir, group_4_windSpeed )
group_5_series_data = self.create_windrose_data( group_5_windDir, group_5_windSpeed )
group_6_series_data = self.create_windrose_data( group_6_windDir, group_6_windSpeed )
# Group all together to get wind frequency percentages
wind_sum = sum(group_0_series_data + group_1_series_data + group_2_series_data + group_3_series_data + group_4_series_data + group_5_series_data + group_6_series_data)
if wind_sum > 0:
y = 0
while y < len(group_0_series_data):
group_0_series_data[y] = round(group_0_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_1_series_data):
group_1_series_data[y] = round(group_1_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_2_series_data):
group_2_series_data[y] = round(group_2_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_3_series_data):
group_3_series_data[y] = round(group_3_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_4_series_data):
group_4_series_data[y] = round(group_4_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_5_series_data):
group_5_series_data[y] = round(group_5_series_data[y] / wind_sum * 100)
y += 1
y = 0
while y < len(group_6_series_data):
group_6_series_data[y] = round(group_6_series_data[y] / wind_sum * 100)
y += 1
# Setup the labels based on unit
if windSpeed_unit == "mile_per_hour" or windSpeed_unit == "mile_per_hour2":
group_0_speedRange = "< 1"
group_1_speedRange = "1-3"
group_2_speedRange = "4-7"
group_3_speedRange = "8-12"
group_4_speedRange = "13-18"
group_5_speedRange = "19-24"
group_6_speedRange = "25+"
elif windSpeed_unit == "km_per_hour" or windSpeed_unit == "km_per_hour2":
group_0_speedRange = "< 2"
group_1_speedRange = "2-5"
group_2_speedRange = "6-11"
group_3_speedRange = "12-19"
group_4_speedRange = "20-28"
group_5_speedRange = "29-38"
group_6_speedRange = "39+"
elif windSpeed_unit == "meter_per_second" or windSpeed_unit == "meter_per_second2":
group_0_speedRange = "< 0.5"
group_1_speedRange = "0.5-1.5"
group_2_speedRange = "1.6-3.3"
group_3_speedRange = "3.4-5.5"
group_4_speedRange = "5.5-7.9"
group_5_speedRange = "8-10.7"
group_6_speedRange = "10.8+"
elif windSpeed_unit == "knot" or windSpeed_unit == "knot2":
group_0_speedRange = "< 1"
group_1_speedRange = "1-3"
group_2_speedRange = "4-6"
group_3_speedRange = "7-10"
group_4_speedRange = "11-16"
group_5_speedRange = "17-21"
group_6_speedRange = "22+"
group_0_name = "%s %s" % (group_0_speedRange, windSpeed_unit_label)
group_1_name = "%s %s" % (group_1_speedRange, windSpeed_unit_label)
group_2_name = "%s %s" % (group_2_speedRange, windSpeed_unit_label)
group_3_name = "%s %s" % (group_3_speedRange, windSpeed_unit_label)
group_4_name = "%s %s" % (group_4_speedRange, windSpeed_unit_label)
group_5_name = "%s %s" % (group_5_speedRange, windSpeed_unit_label)
group_6_name = "%s %s" % (group_6_speedRange, windSpeed_unit_label)
group_0 = { "name": group_0_name,
"type": "column",
"_colorIndex": 0,
"zIndex": 106,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_0_series_data
}
group_1 = { "name": group_1_name,
"type": "column",
"_colorIndex": 1,
"zIndex": 105,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_1_series_data
}
group_2 = { "name": group_2_name,
"type": "column",
"_colorIndex": 2,
"zIndex": 104,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_2_series_data
}
group_3 = { "name": group_3_name,
"type": "column",
"_colorIndex": 3,
"zIndex": 103,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_3_series_data
}
group_4 = { "name": group_4_name,
"type": "column",
"_colorIndex": 4,
"zIndex": 102,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_4_series_data
}
group_5 = { "name": group_5_name,
"type": "column",
"_colorIndex": 5,
"zIndex": 101,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_5_series_data
}
group_6 = { "name": group_6_name,
"type": "column",
"_colorIndex": 6,
"zIndex": 100,
"stacking": "normal",
"fillOpacity": 0.75,
"data": group_6_series_data
}
# Append everything into a list and return right away, do not process rest of function
series = [group_0, group_1, group_2, group_3, group_4, group_5, group_6]
return series
# Special Belchertown Weather Range (radial)
# https://www.highcharts.com/blog/tutorials/209-the-art-of-the-chart-weather-radials/
if observation == "weatherRange":
# Define what we are looking up
if weatherRange_obs_lookup is not None:
obs_lookup = weatherRange_obs_lookup
else:
raise Warning( "Error trying to create the weather range graph. You are missing the range_type configuration item." )
# Force 1 day if aggregate_interval. These charts are meant to show a column range for high, low and average for a full day.
if not aggregate_interval:
aggregate_interval = 86400
# Get min values
aggregate_type = "min"
try:
(time_start_vt, time_stop_vt, obs_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
except Exception as e:
raise Warning( "Error trying to use database binding %s to graph observation %s. Error was: %s." % (binding, obs_lookup, e) )
min_obs_vt = self.converter.convert(obs_vt)
# Get max values
aggregate_type = "max"
try:
(time_start_vt, time_stop_vt, obs_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
except Exception as e:
raise Warning( "Error trying to use database binding %s to graph observation %s. Error was: %s." % (binding, obs_lookup, e) )
max_obs_vt = self.converter.convert(obs_vt)
# Get avg values
aggregate_type = "avg"
try:
(time_start_vt, time_stop_vt, obs_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
except Exception as e:
raise Warning( "Error trying to use database binding %s to graph observation %s. Error was: %s." % (binding, obs_lookup, e) )
avg_obs_vt = self.converter.convert(obs_vt)
obs_unit = avg_obs_vt[1]
obs_unit_label = self.skin_dict['Units']['Labels'].get(obs_unit, "")
# Convert to millis and zip all together
time_ms = [float(x) * 1000 for x in time_start_vt[0]]
output_data = zip(time_ms, min_obs_vt[0], max_obs_vt[0], avg_obs_vt[0])
data = {"weatherRange": True, "obsdata": output_data, "range_unit": obs_unit, "range_unit_label": obs_unit_label}
return data
# Special Belchertown Skin rain counter
if observation == "rainTotal":
obs_lookup = "rain"
# Force sum on this observation
if aggregate_interval:
aggregate_type = "sum"
elif observation == "rainRate":
obs_lookup = "rainRate"
# Force max on this observation
if aggregate_interval:
aggregate_type = "max"
else:
obs_lookup = observation
if xAxis_groupby or len(xAxis_categories) >= 1:
# Setup the converter - for some reason self.converter doesn't work for the group_unit_dict in this section
# Get the target unit nickname (something like 'US' or 'METRIC'):
target_unit_nickname = self.config_dict['StdConvert']['target_unit']
# Get the target unit: weewx.US, weewx.METRIC, weewx.METRICWX
target_unit = weewx.units.unit_constants[target_unit_nickname.upper()]
# Bind to the appropriate standard converter units
converter = weewx.units.StdUnitConverters[target_unit]
# Find what kind of database we're working with and specify the correctly tailored SQL Query for each type of database
data_binding = self.config_dict['StdArchive']['data_binding']
database = self.config_dict['DataBindings'][data_binding]['database']
database_type = self.config_dict['Databases'][database]['database_type']
driver = self.config_dict['DatabaseTypes'][database_type]['driver']
xAxis_labels = []
obsvalues = []
# Define the xAxis group by for the sql query. Default to month
if xAxis_groupby == "hour":
strformat = "%H"
elif xAxis_groupby == "day":
strformat = "%d"
elif xAxis_groupby == "month":
strformat = "%m"
elif xAxis_groupby == "year":
strformat = "%Y"
elif xAxis_groupby == "":
strformat = "%m"
else:
strformat = "%m"
# Default catch all in case the aggregate_type isn't defined, default to sum
if aggregate_type is None:
aggregate_type = "sum"
if driver == "weedb.sqlite":
sql_lookup = 'SELECT strftime("{0}", datetime(dateTime, "unixepoch", "localtime")) as {1}, IFNULL({2}({3}),0) as obs FROM archive WHERE dateTime >= {4} AND dateTime <= {5} GROUP BY {6};'.format( strformat, xAxis_groupby, aggregate_type, obs_lookup, start_ts, end_ts, xAxis_groupby )
elif driver == "weedb.mysql":
sql_lookup = 'SELECT FROM_UNIXTIME( dateTime, "%{0}" ) AS {1}, IFNULL({2}({3}),0) as obs FROM archive WHERE dateTime >= {4} AND dateTime <= {5} GROUP BY {6};'.format( strformat, xAxis_groupby, aggregate_type, obs_lookup, start_ts, end_ts, xAxis_groupby )
# Setup values for the converter
try:
obs_group = weewx.units.obs_group_dict[obs_lookup]
obs_unit_from_target_unit = converter.group_unit_dict[obs_group]
except:
# This observation doesn't exist within weewx schema so nothing to convert, so set None type
obs_group = None
obs_unit_from_target_unit = None
query = archive.genSql( sql_lookup )
for row in query:
xAxis_labels.append( row[0] )
row_tuple = (row[1], obs_unit_from_target_unit, obs_group)
row_converted = self.converter.convert( row_tuple )
obsvalues.append( row_converted[0] )
# If the values are to be mirrored, we need to make them negative
if mirrored_value:
for i in range(len(obsvalues)):
if obsvalues[i] is not None:
obsvalues[i] = -obsvalues[i]
# Return a dict which has the value for if we need to add labels from sql or not.
if len(xAxis_categories) == 0:
data = {"use_sql_labels": True, "xAxis_groupby_labels": xAxis_labels, "obsdata": obsvalues}
else:
data = {"use_sql_labels": False, "xAxis_groupby_labels": "", "obsdata": obsvalues}
return data
# Begin standard observation lookups
try:
(time_start_vt, time_stop_vt, obs_vt) = archive.getSqlVectors(TimeSpan(start_ts, end_ts), obs_lookup, aggregate_type, aggregate_interval)
except Exception as e:
raise Warning( "Error trying to use database binding %s to graph observation %s. Error was: %s." % (binding, obs_lookup, e) )
obs_vt = self.converter.convert(obs_vt)
# Special handling for the rain.
if observation == "rainTotal":
# The weewx "rain" observation is really "bucket tips". This special counter increments the bucket tips over timespan to return rain total.
rain_count = 0
obs_round_vt = []
for rain in obs_vt[0]:
# If the rain value is None or "", add it as 0.0
if rain is None or rain == "":
rain = 0.0
rain_count = rain_count + rain
obs_round_vt.append( round( rain_count, 2 ) )
else:
# Send all other observations through the usual process, except Barometer for finer detail
if observation == "barometer":
usage_round = int(self.skin_dict['Units']['StringFormats'].get(obs_vt[1], "1f")[-2])
obs_round_vt = [round(x,usage_round) if x is not None else None for x in obs_vt[0]]
else:
usage_round = int(self.skin_dict['Units']['StringFormats'].get(obs_vt[2], "2f")[-2])
obs_round_vt = [self.round_none(x, usage_round) for x in obs_vt[0]]
# "Today" charts, "timespan_specific" charts and floating timespan charts have the point timestamp on the stop time so we don't see the
# previous minute in the tooltip. (e.g. 4:59 instead of 5:00)
# Everything else has it on the start time so we don't see the next day in the tooltip (e.g. Jan 2 instead of Jan 1)
if time_length == "today" or time_length == "timespan_specific" or isinstance(time_length, int):
point_timestamp = time_stop_vt
else:
point_timestamp = time_start_vt
# If the values are to be mirrored, we need to make them negative
if mirrored_value:
for i in range(len(obs_round_vt)):
if obs_round_vt[i] is not None:
obs_round_vt[i] = -obs_round_vt[i]
time_ms = [float(x) * 1000 for x in point_timestamp[0]]
data = zip(time_ms, obs_round_vt)
return data
def round_none(self, value, places):
"""Round value to 'places' places but also permit a value of None"""
if value is not None:
try:
value = round(value, places)
except:
value = None
return value
def create_windrose_data(self, windDir_list, windSpeed_list):
# List comprehension borrowed from weewx-wd extension
# Create windrose_list container and initialise to all 0s
windrose_list=[0.0 for x in range(16)]
# Step through each windDir and add corresponding windSpeed to windrose_list
x = 0
while x < len(windDir_list):
# Only want to add windSpeed if both windSpeed and windDir have a value
if windSpeed_list[x] is not None and windDir_list[x] is not None:
# Add the windSpeed value to the corresponding element of our windrose list
windrose_list[int((windDir_list[x]+11.25)/22.5)%16] += windSpeed_list[x]
x += 1
# Step through our windrose list and round all elements to 1 decimal place
y = 0
while y < len(windrose_list):
windrose_list[y] = round(windrose_list[y],1)
y += 1
# Need to return a string of the list elements comma separated, no spaces and bounded by [ and ]
#windroseData = '[' + ','.join(str(z) for z in windrose_list) + ']'
return windrose_list
def get_cardinal_direction(self, degree):
if 0 <= degree <= 11.25:
return "N"
elif 11.26 <= degree <= 33.75:
return "NNE"
elif 33.76 <= degree <= 56.25:
return "NE"
elif 56.26 <= degree <= 78.75:
return "ENE"
elif 78.76 <= degree <= 101.25:
return "E"
elif 101.26 <= degree <= 123.75:
return "ESE"
elif 123.76 <= degree <= 146.25:
return "SE"
elif 146.26 <= degree <= 168.75:
return "SSE"
elif 168.76 <= degree <= 191.25:
return "S"
elif 191.26 <= degree <= 213.75:
return "SSW"
elif 213.76 <= degree <= 236.25:
return "SW"
elif 236.26 <= degree <= 258.75:
return "WSW"
elif 258.76 <= degree <= 281.25:
return "W"
elif 281.26 <= degree <= 303.75:
return "WNW"
elif 303.76 <= degree <= 326.25:
return "NW"
elif 326.26 <= degree <= 348.75:
return "NNW"
elif 348.76 <= degree <= 360:
return "N"
def highcharts_series_options_to_float(self, d):
# Recurse through all the series options and set any strings that should be numbers to float.
# https://stackoverflow.com/a/54565277/1177153
try:
for k, v in d.items():
if isinstance(v, dict):
# Check nested dicts
self.highcharts_series_options_to_float(v)
else:
try:
v = to_float(v)
d.update({k: v})
except:
pass
return d
except:
# This item isn't a dict, so return it back
return d