diff --git a/bin/user/belchertown.py b/bin/user/belchertown.py
index a79cf8ad..2f60087d 100644
--- a/bin/user/belchertown.py
+++ b/bin/user/belchertown.py
@@ -87,6 +87,11 @@ def logerr(msg):
VERSION = "1.2"
loginf("version %s" % VERSION)
+aqi = 0
+aqi_category = ""
+aqi_time = 0
+aqi_location = ""
+
class getData(SearchList):
def __init__(self, generator):
SearchList.__init__(self, generator)
@@ -200,6 +205,12 @@ def get_extension_list(self, timespan, db_lookup):
"""
Build the data needed for the Belchertown skin
"""
+
+ global aqi
+ global aqi_category
+ global aqi_time
+ global aqi_location
+
# Look for the debug flag which can be used to show more logging
weewx.debug = int(self.generator.config_dict.get('debug', 0))
@@ -344,20 +355,33 @@ def get_extension_list(self, timespan, db_lookup):
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']
- if 'radar_zoom' in self.generator.skin_dict['Extras']:
- zoom = self.generator.skin_dict['Extras']['radar_zoom']
- else:
- zoom = "8"
- if 'radar_marker' in self.generator.skin_dict['Extras'] and self.generator.skin_dict['Extras']['radar_marker'] == "1":
- marker = "true"
+ lat = self.generator.config_dict['Station']['latitude']
+ lon = self.generator.config_dict['Station']['longitude']
+ if 'radar_zoom' in self.generator.skin_dict['Extras']:
+ zoom = self.generator.skin_dict['Extras']['radar_zoom']
+ else:
+ zoom = "8"
+ if 'radar_marker' in self.generator.skin_dict['Extras'] and self.generator.skin_dict['Extras']['radar_marker'] == "1":
+ marker = "true"
+ else:
+ marker = ""
+
+ # Set default radar html code, and override with user-specified value if applicable
+ if self.generator.skin_dict['Extras'].get('radar_html') == "":
+ if self.generator.skin_dict['Extras'].get('aeris_map') == "1":
+ radar_html = ' '.format( self.generator.skin_dict['Extras']['forecast_api_id'], self.generator.skin_dict['Extras']['forecast_api_secret'], lat, lon, zoom )
else:
- marker = ""
- radar_html = ''.format( lat, lon, zoom, marker, lat, lon )
+ radar_html = ''.format( lat, lon, zoom, marker, lat, lon )
else:
radar_html = self.generator.skin_dict['Extras']['radar_html']
+
+ if self.generator.skin_dict['Extras'].get('radar_html_dark') == None:
+ if self.generator.skin_dict['Extras'].get('aeris_map') == "1":
+ radar_html_dark = ' '.format( self.generator.skin_dict['Extras']['forecast_api_id'], self.generator.skin_dict['Extras']['forecast_api_secret'], lat, lon, zoom )
+ else:
+ radar_html_dark = "None"
+ else:
+ radar_html_dark = self.generator.skin_dict['Extras']['radar_html_dark']
"""
Build the all time stats.
@@ -390,11 +414,14 @@ def get_extension_list(self, timespan, db_lookup):
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
+ # Find the group_name for outTemp in database
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")
+
+ # Find the group_name for outTemp from the skin.conf
+ skin_outTemp_unit = self.generator.converter.group_unit_dict["group_temperature"]
+
+ # Find the number of decimals to round to based on the skin.conf
+ outTemp_round = self.generator.skin_dict['Units']['StringFormats'].get(skin_outTemp_unit, "%.1f")
# Largest Daily Temperature Range Conversions
# Max temperature for this day
@@ -452,18 +479,21 @@ def get_extension_list(self, timespan, db_lookup):
# Rain lookups
- # Find the group_name for rain
+ # Find the group_name for rain in database
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")
-
+ # Find the group_name for rain in the skin.conf
+ skin_rain_unit = self.generator.converter.group_unit_dict["group_rain"]
+
+ # Find the number of decimals to round the result based on the skin.conf
+ rain_round = self.generator.skin_dict['Units']['StringFormats'].get(skin_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 ]
+ rainiest_day = [ rainiest_day_query[0], locale.format("%g", float(rainiest_day_converted)) ]
else:
rainiest_day = [ calendar.timegm( time.gmtime() ), locale.format("%.2f", 0) ]
@@ -472,7 +502,7 @@ def get_extension_list(self, timespan, db_lookup):
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 ]
+ at_rainiest_day = [ at_rainiest_day_query[0], locale.format("%g", float(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
@@ -666,7 +696,6 @@ def get_extension_list(self, timespan, db_lookup):
"""
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_provider = self.generator.skin_dict['Extras']['forecast_provider']
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']
@@ -821,7 +850,6 @@ def aeris_icon( data ):
"mcloudyw": "mostly-cloudy-day",
"mcloudywn": "mostly-cloudy-night",
"na": "unknown",
- "null": "null",
"pcloudy": "partly-cloudy-day",
"pcloudyn": "partly-cloudy-night",
"pcloudyr": "rain",
@@ -852,6 +880,7 @@ def aeris_icon( data ):
"showers": "rain",
"showersn": "rain",
"showersw": "rain",
+ "showerswn": "rain",
"sleet": "sleet",
"sleetn": "sleet",
"sleetsnow": "sleet",
@@ -886,20 +915,20 @@ def aeris_icon( data ):
forecast_lang = self.generator.skin_dict['Extras']['forecast_lang'].lower()
- if forecast_provider == "aeris":
- if self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
- forecast_current_url = "https://api.aerisapi.com/observations/%s,%s?&format=json&filter=metar&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
- else:
- forecast_current_url = "https://api.aerisapi.com/observations/%s,%s?&format=json&filter=allstations&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
- forecast_24hr_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 )
- forecast_3hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=3hr&limit=8&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
- forecast_1hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=1hr&limit=16&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
- 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_alert_limit, forecast_lang, 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_lang, forecast_api_id, forecast_api_secret )
+ if self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
+ 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 )
+ else:
+ forecast_current_url = "https://api.aerisapi.com/observations/%s,%s?&format=json&filter=allstations&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ forecast_24hr_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 )
+ forecast_3hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=3hr&limit=8&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ forecast_1hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=1hr&limit=16&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ aqi_url = "https://api.aerisapi.com/airquality/closest?p=%s,%s&format=json&radius=50mi&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_alert_limit, forecast_lang, 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_lang, forecast_api_id, forecast_api_secret )
# Determine if the file exists and get it's modified time, enhanced for 1 hr forecast to load close to the hour
if os.path.isfile( forecast_file ):
@@ -931,46 +960,97 @@ def aeris_icon( data ):
forecast_file_result = response.read()
response.close()
else:
- if forecast_provider == "aeris":
- # Current conditions
- req = Request( forecast_current_url, None, headers )
- response = urlopen( req )
- current_page = response.read()
- response.close()
- # 24hr forecast (was Forecast)
- req = Request( forecast_24hr_url, None, headers )
- response = urlopen( req )
- forecast_24hr_page = response.read()
- response.close()
- # 3hr forecast
- req = Request( forecast_3hr_url, None, headers )
- response = urlopen( req )
- forecast_3hr_page = response.read()
- response.close()
- # 1hr forecast
- req = Request( forecast_1hr_url, None, headers )
+ # Current conditions
+ req = Request( forecast_current_url, None, headers )
+ response = urlopen( req )
+ current_page = response.read()
+ response.close()
+ # 24hr forecast (was Forecast)
+ req = Request( forecast_24hr_url, None, headers )
+ response = urlopen( req )
+ forecast_24hr_page = response.read()
+ response.close()
+ # 3hr forecast
+ req = Request( forecast_3hr_url, None, headers )
+ response = urlopen( req )
+ forecast_3hr_page = response.read()
+ response.close()
+ # 1hr forecast
+ req = Request( forecast_1hr_url, None, headers )
+ response = urlopen( req )
+ forecast_1hr_page = response.read()
+ response.close()
+ # AQI
+ req = Request( aqi_url, None, headers )
+ response = urlopen( req )
+ aqi_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 )
- forecast_1hr_page = response.read()
+ alerts_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_24hr": [json.loads(forecast_24hr_page)], "forecast_3hr": [json.loads(forecast_3hr_page)], "forecast_1hr": [json.loads(forecast_1hr_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_24hr": [json.loads(forecast_24hr_page.decode('utf-8'))], "forecast_3hr": [json.loads(forecast_3hr_page.decode('utf-8'))], "forecast_1hr": [json.loads(forecast_1hr_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_24hr": [json.loads(forecast_24hr_page)], "forecast_3hr": [json.loads(forecast_3hr_page)], "forecast_1hr": [json.loads(forecast_1hr_page)]} )
- except:
- forecast_file_result = json.dumps( {"timestamp": int(time.time()), "current": [json.loads(current_page.decode('utf-8'))], "forecast_24hr": [json.loads(forecast_24hr_page.decode('utf-8'))], "forecast_3hr": [json.loads(forecast_3hr_page.decode('utf-8'))], "forecast_1hr": [json.loads(forecast_1hr_page.decode('utf-8'))]} )
-
+
+ # 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_24hr":
+ [json.loads(forecast_24hr_page)],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page)],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page)],
+ "alerts":
+ [json.loads(alerts_page)],
+ "aqi":
+ [json.loads(aqi_page)]} )
+ except:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page.decode('utf-8'))],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page.decode('utf-8'))],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page.decode('utf-8'))],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page.decode('utf-8'))],
+ "alerts":
+ [json.loads(alerts_page.decode('utf-8'))],
+ "aqi":
+ [json.loads(aqi_page.decode('utf-8'))]} )
+ else:
+ try:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page)],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page)],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page)],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page)],
+ "aqi":
+ [json.loads(aqi_page)]} )
+ except:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page.decode('utf-8'))],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page.decode('utf-8'))],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page.decode('utf-8'))],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page.decode('utf-8'))],
+ "aqi":
+ [json.loads(aqi_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_24hr_url, error ) )
@@ -990,46 +1070,154 @@ def aeris_icon( data ):
with open( forecast_file, "r" ) as read_file:
data = json.load( read_file )
- if forecast_provider == "aeris":
- if len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "0":
- # Non-metar responses do not contain these values. Set them to empty.
- current_obs_summary = ""
- current_obs_icon = "null.png"
- visibility = "N/A"
- visibility_unit = ""
- elif len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
- 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 = ""
+ try:
+ aqi = data['aqi'][0]['response'][0]['periods'][0]['aqi']
+ aqi_category = data['aqi'][0]['response'][0]['periods'][0]['category']
+ aqi_time = data['aqi'][0]['response'][0]['periods'][0]['timestamp']
+ aqi_location = data['aqi'][0]['response'][0]['place']['name'].title()
+ except Exception as error:
+ logerr( "Error getting AQI from Aeris weather. The error was:\n%s\nThe response from the Aeris AQI server was:\n%s\nThe URL being used is:\n%s" % (error, data['aqi'], aqi_url))
+
+ # Substitute label names if defined in config files, to allow users to supply their own translations
+ # see https://www.aerisweather.com/support/docs/api/reference/endpoints/airquality/
+ if aqi_category == "good":
+ if label_dict["aqi_good"] not in ("aqi_good", ""):
+ aqi_category = label_dict["aqi_good"]
+ else:
+ aqi_category = "good"
+ elif aqi_category == "moderate":
+ if label_dict["aqi_moderate"] not in ("aqi_moderate", ""):
+ aqi_category = label_dict["aqi_moderate"]
+ else:
+ aqi_category = "moderate"
+ elif aqi_category == "usg":
+ if label_dict["aqi_usg"] not in ("aqi_usg", ""):
+ aqi_category = label_dict["aqi_usg"]
+ else:
+ aqi_category = "unhealthy for some"
+ elif aqi_category == "unhealthy":
+ if label_dict["aqi_unhealthy"] not in ("aqi_unhealthy", ""):
+ aqi_category = label_dict["aqi_unhealthy"]
+ else:
+ aqi_category = "unhealthy"
+ elif aqi_category == "very unhealthy":
+ if label_dict["aqi_very_unhealthy"] not in ("aqi_very_unhealthy", ""):
+ aqi_category = label_dict["aqi_very_unhealthy"]
+ else:
+ aqi_category = "very unhealthy"
+ elif aqi_category == "hazardous":
+ if label_dict["aqi_hazardous"] not in ("aqi_hazardous", ""):
+ aqi_category = label_dict["aqi_hazardous"]
+ else:
+ aqi_category = "hazardous"
+ else:
+ aqi_category = "unknown"
+
+ if label_dict["beaufort0"] != "beaufort0":
+ beaufort0 = label_dict["beaufort0"]
+ else:
+ beaufort0 = "calm"
+ if label_dict["beaufort1"] != "beaufort1":
+ beaufort1 = label_dict["beaufort1"]
+ else:
+ beaufort1 = "light air"
+ if label_dict["beaufort2"] != "beaufort2":
+ beaufort2 = label_dict["beaufort2"]
+ else:
+ beaufort2 = "light breeze"
+ if label_dict["beaufort3"] != "beaufort3":
+ beaufort3 = label_dict["beaufort3"]
+ else:
+ beaufort3 = "gentle breeze"
+ if label_dict["beaufort4"] != "beaufort4":
+ beaufort4 = label_dict["beaufort4"]
+ else:
+ beaufort4 = "moderate breeze"
+ if label_dict["beaufort5"] != "beaufort5":
+ beaufort5 = label_dict["beaufort5"]
+ else:
+ beaufort5 = "fresh breeze"
+ if label_dict["beaufort6"] != "beaufort6":
+ beaufort6 = label_dict["beaufort6"]
+ else:
+ beaufort6 = "strong breeze"
+ if label_dict["beaufort7"] != "beaufort7":
+ beaufort7 = label_dict["beaufort7"]
+ else:
+ beaufort7 = "near gale"
+ if label_dict["beaufort8"] != "beaufort8":
+ beaufort8 = label_dict["beaufort8"]
+ else:
+ beaufort8 = "gale"
+ if label_dict["beaufort9"] != "beaufort9":
+ beaufort9 = label_dict["beaufort9"]
+ else:
+ beaufort9 = "strong gale"
+ if label_dict["beaufort10"] != "beaufort10":
+ beaufort10 = label_dict["beaufort10"]
+ else:
+ beaufort10 = "storm"
+ if label_dict["beaufort11"] != "beaufort11":
+ beaufort11 = label_dict["beaufort11"]
+ else:
+ beaufort11 = "violent storm"
+ if label_dict["beaufort12"] != "beaufort12":
+ beaufort12 = label_dict["beaufort12"]
+ else:
+ beaufort12 = "hurricane force"
+
+ if len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "0":
+ # Non-metar responses do not contain these values. Set them to empty.
+ current_obs_summary = ""
+ current_obs_icon = ""
+ visibility = "N/A"
+ visibility_unit = ""
+ elif len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
+ 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:
- # 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 = ""
+ visibility = "N/A"
+ visibility_unit = ""
else:
- # If the user selected to not use METAR, then these observations are null.
- # If there's no data in the ob array then it's probably because of an error. Example:
- # "code": "warn_no_data",
- # "description": "Valid request. No results available based on your query parameters."
- current_obs_summary = ""
- current_obs_icon = "null.png"
- visibility = "N/A"
- visibility_unit = ""
+ # 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:
+ # If the user selected to not use METAR, then these observations are null.
+ # If there's no data in the ob array then it's probably because of an error. Example:
+ # "code": "warn_no_data",
+ # "description": "Valid request. No results available based on your query parameters."
+ current_obs_summary = ""
+ current_obs_icon = ""
+ visibility = "N/A"
+ visibility_unit = ""
else:
- current_obs_icon = "null.png"
+ current_obs_icon = ""
current_obs_summary = ""
visibility = "N/A"
visibility_unit = ""
+ beaufort0 = ""
+ beaufort1 = ""
+ beaufort2 = ""
+ beaufort3 = ""
+ beaufort4 = ""
+ beaufort5 = ""
+ beaufort6 = ""
+ beaufort7 = ""
+ beaufort8 = ""
+ beaufort9 = ""
+ beaufort10 = ""
+ beaufort11 = ""
+ beaufort12 = ""
"""
@@ -1049,7 +1237,8 @@ def aeris_icon( data ):
if self.generator.skin_dict['Extras']['earthquake_server'] == "USGS":
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 )
elif self.generator.skin_dict['Extras']['earthquake_server'] == "GeoNet":
- earthquake_url = "https://api.geonet.org.nz/quake?MMI=4"
+ earthquake_url = ("https://api.geonet.org.nz/quake?MMI=%s"
+ % self.generator.skin_dict['Extras']['geonet_mmi'])
earthquake_is_stale = False
# Determine if the file exists and get it's modified time
@@ -1121,19 +1310,12 @@ def aeris_icon( data ):
elif self.generator.skin_dict['Extras']['earthquake_server'] == "GeoNet":
eqtime = eqdata["features"][0]["properties"]["time"]
#convert time to UNIX format
- eqtime = eqtime.replace("Z","")
- try:
- # Python 3.7+
- eqtime = datetime.datetime.fromisoformat(eqtime)
- except:
- # Python 2/3.6:
- from dateutil import parser
- eqtime = parser.isoparse(eqtime)
- eqtime = int(eqtime.replace(tzinfo=datetime.timezone.utc).timestamp())
+ eqtime = datetime.datetime.strptime(eqtime, "%Y-%m-%dT%H:%M:%S.%fZ")
+ eqtime = int((eqtime-datetime.datetime(1970,1,1)).total_seconds())
equrl = ("https://www.geonet.org.nz/earthquake/" +
eqdata["features"][0]["properties"]["publicID"])
eqplace = eqdata["features"][0]["properties"]["locality"]
- eqmag = locale.format("%g", float(round(eqdata["features"][0]["properties"]["magnitude"],1)) )
+ eqmag = locale.format("%g", float(round(eqdata["features"][0]["properties"]["magnitude"],1)) )
eqlat = str( round( eqdata["features"][0]["geometry"]["coordinates"][1], 4 ) )
eqlon = str( round( eqdata["features"][0]["geometry"]["coordinates"][0], 4 ) )
eqdistance_bearing = self.get_gps_distance((float(latitude), float(longitude)),
@@ -1335,60 +1517,79 @@ def aeris_icon( data ):
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,
- 'earthquake_distance_away': eqdistance,
- 'earthquake_distance_label': eq_distance_label,
- 'earthquake_bearing': eqbearing,
- 'earthquake_bearing_raw': eqbearing_raw,
- 'social_html': social_html,
- 'custom_css_exists': custom_css_exists }
+ 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,
+ 'radar_html_dark': radar_html_dark,
+ '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,
+ 'earthquake_distance_away': eqdistance,
+ 'earthquake_distance_label': eq_distance_label,
+ 'earthquake_bearing': eqbearing,
+ 'earthquake_bearing_raw': eqbearing_raw,
+ 'social_html': social_html,
+ 'custom_css_exists': custom_css_exists,
+ 'aqi': aqi,
+ 'aqi_category': aqi_category,
+ 'aqi_location': aqi_location,
+ 'beaufort0': beaufort0,
+ 'beaufort1': beaufort1,
+ 'beaufort2': beaufort2,
+ 'beaufort3': beaufort3,
+ 'beaufort4': beaufort4,
+ 'beaufort5': beaufort5,
+ 'beaufort6': beaufort6,
+ 'beaufort7': beaufort7,
+ 'beaufort8': beaufort8,
+ 'beaufort9': beaufort9,
+ 'beaufort10': beaufort10,
+ 'beaufort11': beaufort11,
+ 'beaufort12': beaufort12
+ }
# Finally, return our extension as a list:
return [search_list_extension]
@@ -1990,6 +2191,29 @@ def get_observation_data(self, binding, archive, observation, start_ts, end_ts,
elif windData[1] >= 22:
group_6_windDir.append( windData[0] )
group_6_windSpeed.append( windData[1] )
+ elif windSpeed_unit == "beaufort":
+ if windData[1] <= 1:
+ group_0_windDir.append( windData[0] )
+ group_0_windSpeed.append( windData[1] )
+ elif windData[1] == 2:
+ group_1_windDir.append( windData[0] )
+ group_1_windSpeed.append( windData[1] )
+ elif windData[1] == 3:
+ group_2_windDir.append( windData[0] )
+ group_2_windSpeed.append( windData[1] )
+ elif windData[1] == 4:
+ group_3_windDir.append( windData[0] )
+ group_3_windSpeed.append( windData[1] )
+ elif windData[1] == 5:
+ group_4_windDir.append( windData[0] )
+ group_4_windSpeed.append( windData[1] )
+ elif windData[1] == 6:
+ group_5_windDir.append( windData[0] )
+ group_5_windSpeed.append( windData[1] )
+ elif windData[1] >= 7:
+ 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 )
@@ -2065,7 +2289,15 @@ def get_observation_data(self, binding, archive, observation, start_ts, end_ts,
group_4_speedRange = "11-16"
group_5_speedRange = "17-21"
group_6_speedRange = "22+"
-
+ elif windSpeed_unit == "beaufort":
+ group_0_speedRange = "0"
+ group_1_speedRange = "1"
+ group_2_speedRange = "2"
+ group_3_speedRange = "3"
+ group_4_speedRange = "4"
+ group_5_speedRange = "5"
+ group_6_speedRange = "6+"
+
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)
@@ -2187,6 +2419,10 @@ def get_observation_data(self, binding, archive, observation, start_ts, end_ts,
return data
+ if observation == "aqiChart":
+ data = { "aqiChart": True, "obsdata": [{'y': aqi, 'category': aqi_category}] }
+ return data
+
# Hays chart
if observation == "haysChart":
@@ -2349,7 +2585,7 @@ def get_observation_data(self, binding, archive, observation, start_ts, end_ts,
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])
+ usage_round = int(self.skin_dict['Units']['StringFormats'].get(obs_vt[1], "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
diff --git a/bin/user/belchertown.py.mju b/bin/user/belchertown.py.mju
new file mode 100644
index 00000000..2f60087d
--- /dev/null
+++ b/bin/user/belchertown.py.mju
@@ -0,0 +1,2710 @@
+# 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 math import atan2, degrees, radians, cos, sin, asin, sqrt
+
+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, archiveSpanSpan, 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.2"
+loginf("version %s" % VERSION)
+
+aqi = 0
+aqi_category = ""
+aqi_time = 0
+aqi_location = ""
+
+class getData(SearchList):
+ def __init__(self, generator):
+ SearchList.__init__(self, generator)
+
+ def get_gps_distance(self, pointA, pointB, distance_unit):
+ # https://www.geeksforgeeks.org/program-distance-two-points-earth/ and https://stackoverflow.com/a/43960736
+ # The math module contains a function named radians which converts from degrees to radians.
+ if (type(pointA) != tuple) or (type(pointB) != tuple):
+ raise TypeError("Only tuples are supported as arguments")
+ lat1 = pointA[0]
+ lon1 = pointA[1]
+ lat2 = pointB[0]
+ lon2 = pointB[1]
+ # convert decimal degrees to radians
+ lat1r, lon1r, lat2r, lon2r = map(radians, [lat1, lon1, lat2, lon2])
+ # Haversine formula
+ dlat = lat2r - lat1r
+ dlon = lon2r - lon1r
+ a = sin(dlat / 2)**2 + cos(lat1r) * cos(lat2r) * sin(dlon / 2)**2
+ c = 2 * asin(sqrt(a))
+ # Radius of earth in kilometers is 6371. Use 3956 for miles
+ if distance_unit == "km":
+ r = 6371
+ else:
+ # Assume mile
+ r = 3956
+ bearing = self.get_gps_bearing(pointA, pointB)
+ # Returns distance as object 0 and bearing as object 1
+ return [(c * r), self.get_cardinal_direction(bearing), bearing]
+
+ def get_gps_bearing(self, pointA, pointB):
+ """
+ https://gist.github.com/jeromer/2005586
+ Calculates the bearing between two points.
+ :Parameters:
+ - pointA: The tuple representing the latitude/longitude for the
+ first point. Latitude and longitude must be in decimal degrees
+ - pointB: The tuple representing the latitude/longitude for the
+ second point. Latitude and longitude must be in decimal degrees
+ :Returns:
+ The bearing in degrees
+ :Returns Type:
+ float
+ """
+ if (type(pointA) != tuple) or (type(pointB) != tuple):
+ raise TypeError("Only tuples are supported as arguments")
+ lat1 = radians(pointA[0])
+ lat2 = radians(pointB[0])
+ diffLong = radians(pointB[1] - pointA[1])
+ x = sin(diffLong) * cos(lat2)
+ y = cos(lat1) * sin(lat2) - (sin(lat1)
+ * cos(lat2) * cos(diffLong))
+ initial_bearing = atan2(x, y)
+ # Now we have the initial bearing but math.atan2 return values
+ # from -180 to + 180 degrees which is not what we want for a compass bearing
+ # The solution is to normalize the initial bearing as shown below
+ initial_bearing = degrees(initial_bearing)
+ compass_bearing = (initial_bearing + 360) % 360
+ return compass_bearing
+
+ def get_cardinal_direction(self, degree, return_only_labels=False):
+ 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
+
+ if return_only_labels:
+ return ordinate_names
+
+ if 0 <= degree <= 11.25:
+ return ordinate_names[0]
+ elif 11.26 <= degree <= 33.75:
+ return ordinate_names[1]
+ elif 33.76 <= degree <= 56.25:
+ return ordinate_names[2]
+ elif 56.26 <= degree <= 78.75:
+ return ordinate_names[3]
+ elif 78.76 <= degree <= 101.25:
+ return ordinate_names[4]
+ elif 101.26 <= degree <= 123.75:
+ return ordinate_names[5]
+ elif 123.76 <= degree <= 146.25:
+ return ordinate_names[6]
+ elif 146.26 <= degree <= 168.75:
+ return ordinate_names[7]
+ elif 168.76 <= degree <= 191.25:
+ return ordinate_names[8]
+ elif 191.26 <= degree <= 213.75:
+ return ordinate_names[9]
+ elif 213.76 <= degree <= 236.25:
+ return ordinate_names[10]
+ elif 236.26 <= degree <= 258.75:
+ return ordinate_names[11]
+ elif 258.76 <= degree <= 281.25:
+ return ordinate_names[12]
+ elif 281.26 <= degree <= 303.75:
+ return ordinate_names[13]
+ elif 303.76 <= degree <= 326.25:
+ return ordinate_names[14]
+ elif 326.26 <= degree <= 348.75:
+ return ordinate_names[15]
+ elif 348.76 <= degree <= 360:
+ return ordinate_names[0]
+
+ def get_extension_list(self, timespan, db_lookup):
+ """
+ Build the data needed for the Belchertown skin
+ """
+
+ global aqi
+ global aqi_category
+ global aqi_time
+ global aqi_location
+
+
+ # 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
+ ordinate_names = self.get_cardinal_direction("", True)
+
+ # 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 += '' + button_text + ' '
+ 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.
+ lat = self.generator.config_dict['Station']['latitude']
+ lon = self.generator.config_dict['Station']['longitude']
+ if 'radar_zoom' in self.generator.skin_dict['Extras']:
+ zoom = self.generator.skin_dict['Extras']['radar_zoom']
+ else:
+ zoom = "8"
+ if 'radar_marker' in self.generator.skin_dict['Extras'] and self.generator.skin_dict['Extras']['radar_marker'] == "1":
+ marker = "true"
+ else:
+ marker = ""
+
+ # Set default radar html code, and override with user-specified value if applicable
+ if self.generator.skin_dict['Extras'].get('radar_html') == "":
+ if self.generator.skin_dict['Extras'].get('aeris_map') == "1":
+ radar_html = ' '.format( self.generator.skin_dict['Extras']['forecast_api_id'], self.generator.skin_dict['Extras']['forecast_api_secret'], lat, lon, zoom )
+ else:
+ radar_html = ''.format( lat, lon, zoom, marker, lat, lon )
+ else:
+ radar_html = self.generator.skin_dict['Extras']['radar_html']
+
+ if self.generator.skin_dict['Extras'].get('radar_html_dark') == None:
+ if self.generator.skin_dict['Extras'].get('aeris_map') == "1":
+ radar_html_dark = ' '.format( self.generator.skin_dict['Extras']['forecast_api_id'], self.generator.skin_dict['Extras']['forecast_api_secret'], lat, lon, zoom )
+ else:
+ radar_html_dark = "None"
+ else:
+ radar_html_dark = self.generator.skin_dict['Extras']['radar_html_dark']
+
+ """
+ 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 in database
+ outTemp_unit = converter.group_unit_dict["group_temperature"]
+
+ # Find the group_name for outTemp from the skin.conf
+ skin_outTemp_unit = self.generator.converter.group_unit_dict["group_temperature"]
+
+ # Find the number of decimals to round to based on the skin.conf
+ outTemp_round = self.generator.skin_dict['Units']['StringFormats'].get(skin_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 in database
+ rain_unit = converter.group_unit_dict["group_rain"]
+
+ # Find the group_name for rain in the skin.conf
+ skin_rain_unit = self.generator.converter.group_unit_dict["group_rain"]
+
+ # Find the number of decimals to round the result based on the skin.conf
+ rain_round = self.generator.skin_dict['Units']['StringFormats'].get(skin_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], locale.format("%g", float(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], locale.format("%g", float(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 += '%s :' % ( y, y )
+ else:
+ noaa_header_html += '%s :' % 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 += ' %s ' % ( y, month_num, month_abbr )
+ else:
+ noaa_header_html += ' %s ' % month_abbr
+
+ # Row build complete, push next row to new line
+ noaa_header_html += " "
+
+ # 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": "mostly-clear-day",
+ "fairn": "mostly-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": "mostly-cloudy-day",
+ "mcloudyn": "mostly-cloudy-night",
+ "mcloudyr": "rain",
+ "mcloudyrn": "rain",
+ "mcloudyrw": "rain",
+ "mcloudyrwn": "rain",
+ "mcloudys": "snow",
+ "mcloudysn": "snow",
+ "mcloudysf": "snow",
+ "mcloudysfn": "snow",
+ "mcloudysfw": "snow",
+ "mcloudysfwn": "snow",
+ "mcloudysw": "mostly-cloudy-day",
+ "mcloudyswn": "mostly-cloudy-night",
+ "mcloudyt": "thunderstorm",
+ "mcloudytn": "thunderstorm",
+ "mcloudytw": "thunderstorm",
+ "mcloudytwn": "thunderstorm",
+ "mcloudyw": "mostly-cloudy-day",
+ "mcloudywn": "mostly-cloudy-night",
+ "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",
+ "showers": "rain",
+ "showersn": "rain",
+ "showersw": "rain",
+ "showerswn": "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": "mostly-clear-day",
+ "sunnyn": "mostly-clear-night",
+ "sunnyw": "mostly-clear-day",
+ "sunnywn": "mostly-clear-night",
+ "tstorm": "thunderstorm",
+ "tstormn": "thunderstorm",
+ "tstorms": "thunderstorm",
+ "tstormsn": "thunderstorm",
+ "tstormsw": "thunderstorm",
+ "tstormswn": "thunderstorm",
+ "wind": "wind",
+ "wintrymix": "sleet",
+ "wintrymixn": "sleet"
+ }
+ return icon_dict[icon_name]
+
+
+ forecast_lang = self.generator.skin_dict['Extras']['forecast_lang'].lower()
+ if self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
+ 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 )
+ else:
+ forecast_current_url = "https://api.aerisapi.com/observations/%s,%s?&format=json&filter=allstations&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ forecast_24hr_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 )
+ forecast_3hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=3hr&limit=8&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ forecast_1hr_url = "https://api.aerisapi.com/forecasts/%s,%s?&format=json&filter=1hr&limit=16&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ aqi_url = "https://api.aerisapi.com/airquality/closest?p=%s,%s&format=json&radius=50mi&limit=1&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_api_id, forecast_api_secret )
+ 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_alert_limit, forecast_lang, 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&lang=%s&client_id=%s&client_secret=%s" % ( latitude, longitude, forecast_lang, forecast_api_id, forecast_api_secret )
+
+ # Determine if the file exists and get it's modified time, enhanced for 1 hr forecast to load close to the hour
+ 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:
+ if ( time.strftime("%M") < "05" and int( time.time() ) - int( os.path.getmtime( forecast_file ) ) ) > int( 300 ) : # catches repeated calls to this function every archive interval (300secs)
+ 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()
+ # 24hr forecast (was Forecast)
+ req = Request( forecast_24hr_url, None, headers )
+ response = urlopen( req )
+ forecast_24hr_page = response.read()
+ response.close()
+ # 3hr forecast
+ req = Request( forecast_3hr_url, None, headers )
+ response = urlopen( req )
+ forecast_3hr_page = response.read()
+ response.close()
+ # 1hr forecast
+ req = Request( forecast_1hr_url, None, headers )
+ response = urlopen( req )
+ forecast_1hr_page = response.read()
+ response.close()
+ # AQI
+ req = Request( aqi_url, None, headers )
+ response = urlopen( req )
+ aqi_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_24hr":
+ [json.loads(forecast_24hr_page)],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page)],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page)],
+ "alerts":
+ [json.loads(alerts_page)],
+ "aqi":
+ [json.loads(aqi_page)]} )
+ except:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page.decode('utf-8'))],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page.decode('utf-8'))],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page.decode('utf-8'))],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page.decode('utf-8'))],
+ "alerts":
+ [json.loads(alerts_page.decode('utf-8'))],
+ "aqi":
+ [json.loads(aqi_page.decode('utf-8'))]} )
+ else:
+ try:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page)],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page)],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page)],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page)],
+ "aqi":
+ [json.loads(aqi_page)]} )
+ except:
+ forecast_file_result = json.dumps( {"timestamp":
+ int(time.time()),
+ "current":
+ [json.loads(current_page.decode('utf-8'))],
+ "forecast_24hr":
+ [json.loads(forecast_24hr_page.decode('utf-8'))],
+ "forecast_3hr":
+ [json.loads(forecast_3hr_page.decode('utf-8'))],
+ "forecast_1hr":
+ [json.loads(forecast_1hr_page.decode('utf-8'))],
+ "aqi":
+ [json.loads(aqi_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_24hr_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 )
+
+ try:
+ aqi = data['aqi'][0]['response'][0]['periods'][0]['aqi']
+ aqi_category = data['aqi'][0]['response'][0]['periods'][0]['category']
+ aqi_time = data['aqi'][0]['response'][0]['periods'][0]['timestamp']
+ aqi_location = data['aqi'][0]['response'][0]['place']['name'].title()
+ except Exception as error:
+ logerr( "Error getting AQI from Aeris weather. The error was:\n%s\nThe response from the Aeris AQI server was:\n%s\nThe URL being used is:\n%s" % (error, data['aqi'], aqi_url))
+
+ # Substitute label names if defined in config files, to allow users to supply their own translations
+ # see https://www.aerisweather.com/support/docs/api/reference/endpoints/airquality/
+ if aqi_category == "good":
+ if label_dict["aqi_good"] not in ("aqi_good", ""):
+ aqi_category = label_dict["aqi_good"]
+ else:
+ aqi_category = "good"
+ elif aqi_category == "moderate":
+ if label_dict["aqi_moderate"] not in ("aqi_moderate", ""):
+ aqi_category = label_dict["aqi_moderate"]
+ else:
+ aqi_category = "moderate"
+ elif aqi_category == "usg":
+ if label_dict["aqi_usg"] not in ("aqi_usg", ""):
+ aqi_category = label_dict["aqi_usg"]
+ else:
+ aqi_category = "unhealthy for some"
+ elif aqi_category == "unhealthy":
+ if label_dict["aqi_unhealthy"] not in ("aqi_unhealthy", ""):
+ aqi_category = label_dict["aqi_unhealthy"]
+ else:
+ aqi_category = "unhealthy"
+ elif aqi_category == "very unhealthy":
+ if label_dict["aqi_very_unhealthy"] not in ("aqi_very_unhealthy", ""):
+ aqi_category = label_dict["aqi_very_unhealthy"]
+ else:
+ aqi_category = "very unhealthy"
+ elif aqi_category == "hazardous":
+ if label_dict["aqi_hazardous"] not in ("aqi_hazardous", ""):
+ aqi_category = label_dict["aqi_hazardous"]
+ else:
+ aqi_category = "hazardous"
+ else:
+ aqi_category = "unknown"
+
+ if label_dict["beaufort0"] != "beaufort0":
+ beaufort0 = label_dict["beaufort0"]
+ else:
+ beaufort0 = "calm"
+ if label_dict["beaufort1"] != "beaufort1":
+ beaufort1 = label_dict["beaufort1"]
+ else:
+ beaufort1 = "light air"
+ if label_dict["beaufort2"] != "beaufort2":
+ beaufort2 = label_dict["beaufort2"]
+ else:
+ beaufort2 = "light breeze"
+ if label_dict["beaufort3"] != "beaufort3":
+ beaufort3 = label_dict["beaufort3"]
+ else:
+ beaufort3 = "gentle breeze"
+ if label_dict["beaufort4"] != "beaufort4":
+ beaufort4 = label_dict["beaufort4"]
+ else:
+ beaufort4 = "moderate breeze"
+ if label_dict["beaufort5"] != "beaufort5":
+ beaufort5 = label_dict["beaufort5"]
+ else:
+ beaufort5 = "fresh breeze"
+ if label_dict["beaufort6"] != "beaufort6":
+ beaufort6 = label_dict["beaufort6"]
+ else:
+ beaufort6 = "strong breeze"
+ if label_dict["beaufort7"] != "beaufort7":
+ beaufort7 = label_dict["beaufort7"]
+ else:
+ beaufort7 = "near gale"
+ if label_dict["beaufort8"] != "beaufort8":
+ beaufort8 = label_dict["beaufort8"]
+ else:
+ beaufort8 = "gale"
+ if label_dict["beaufort9"] != "beaufort9":
+ beaufort9 = label_dict["beaufort9"]
+ else:
+ beaufort9 = "strong gale"
+ if label_dict["beaufort10"] != "beaufort10":
+ beaufort10 = label_dict["beaufort10"]
+ else:
+ beaufort10 = "storm"
+ if label_dict["beaufort11"] != "beaufort11":
+ beaufort11 = label_dict["beaufort11"]
+ else:
+ beaufort11 = "violent storm"
+ if label_dict["beaufort12"] != "beaufort12":
+ beaufort12 = label_dict["beaufort12"]
+ else:
+ beaufort12 = "hurricane force"
+
+ if len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "0":
+ # Non-metar responses do not contain these values. Set them to empty.
+ current_obs_summary = ""
+ current_obs_icon = ""
+ visibility = "N/A"
+ visibility_unit = ""
+ elif len(data["current"][0]["response"]) > 0 and self.generator.skin_dict['Extras']['forecast_aeris_use_metar'] == "1":
+ 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:
+ # If the user selected to not use METAR, then these observations are null.
+ # If there's no data in the ob array then it's probably because of an error. Example:
+ # "code": "warn_no_data",
+ # "description": "Valid request. No results available based on your query parameters."
+ current_obs_summary = ""
+ current_obs_icon = ""
+ visibility = "N/A"
+ visibility_unit = ""
+ else:
+ current_obs_icon = ""
+ current_obs_summary = ""
+ visibility = "N/A"
+ visibility_unit = ""
+ beaufort0 = ""
+ beaufort1 = ""
+ beaufort2 = ""
+ beaufort3 = ""
+ beaufort4 = ""
+ beaufort5 = ""
+ beaufort6 = ""
+ beaufort7 = ""
+ beaufort8 = ""
+ beaufort9 = ""
+ beaufort10 = ""
+ beaufort11 = ""
+ beaufort12 = ""
+
+
+ """
+ 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']
+ distance_unit = self.generator.converter.group_unit_dict["group_distance"]
+ eq_distance_label = self.generator.skin_dict['Units']['Labels'].get(distance_unit, "")
+ eq_distance_round = self.generator.skin_dict['Units']['StringFormats'].get(distance_unit, "%.1f")
+ 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
+ if self.generator.skin_dict['Extras']['earthquake_server'] == "USGS":
+ 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 )
+ elif self.generator.skin_dict['Extras']['earthquake_server'] == "GeoNet":
+ earthquake_url = ("https://api.geonet.org.nz/quake?MMI=%s"
+ % self.generator.skin_dict['Extras']['geonet_mmi'])
+ 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:
+ # Python 2/3
+ try:
+ file.write( page.encode('utf-8') )
+ except:
+ 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:
+ if self.generator.skin_dict['Extras']['earthquake_server'] == "USGS":
+ eqtime = eqdata["features"][0]["properties"]["time"] / 1000
+ equrl = eqdata["features"][0]["properties"]["url"]
+ eqplace = eqdata["features"][0]["properties"]["place"]
+ eqmag = locale.format("%g", float(eqdata["features"][0]["properties"]["mag"]) )
+ elif self.generator.skin_dict['Extras']['earthquake_server'] == "GeoNet":
+ eqtime = eqdata["features"][0]["properties"]["time"]
+ #convert time to UNIX format
+ eqtime = datetime.datetime.strptime(eqtime, "%Y-%m-%dT%H:%M:%S.%fZ")
+ eqtime = int((eqtime-datetime.datetime(1970,1,1)).total_seconds())
+ equrl = ("https://www.geonet.org.nz/earthquake/" +
+ eqdata["features"][0]["properties"]["publicID"])
+ eqplace = eqdata["features"][0]["properties"]["locality"]
+ eqmag = locale.format("%g", float(round(eqdata["features"][0]["properties"]["magnitude"],1)) )
+ eqlat = str( round( eqdata["features"][0]["geometry"]["coordinates"][1], 4 ) )
+ eqlon = str( round( eqdata["features"][0]["geometry"]["coordinates"][0], 4 ) )
+ eqdistance_bearing = self.get_gps_distance((float(latitude), float(longitude)),
+ (float(eqlat), float(eqlon)),
+ distance_unit)
+ eqdistance = locale.format("%g", float(eq_distance_round % eqdistance_bearing[0]) )
+ eqbearing = eqdistance_bearing[1]
+ eqbearing_raw = eqdistance_bearing[2]
+ except:
+ # No earthquake data
+ eqtime = label_dict["earthquake_no_data"]
+ equrl = ""
+ eqplace = ""
+ eqmag = ""
+ eqlat = ""
+ eqlon = ""
+ eqdistance = ""
+ eqbearing = ""
+ eqbearing_raw = ""
+
+ else:
+ eqtime = ""
+ equrl = ""
+ eqplace = ""
+ eqmag = ""
+ eqlat = ""
+ eqlon = ""
+ eqdistance = ""
+ eqbearing = ""
+ eqbearing_raw = ""
+ eq_distance_label = ""
+
+
+ """
+ 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 = "%s " % str(dayRain_sum)
+ obs_rain_output += " "
+ obs_rain_output += "%s " % 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 += "
"
+ station_obs_html += "%s " % label_dict[obs]
+ station_obs_html += ""
+ if obs == "rainWithRainRate":
+ # Add special rain + rainRate one liner
+ station_obs_html += obs_rain_output
+ else:
+ station_obs_html += "%s " % ( 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 += ' ' # Maintain leading spacing
+ if str(obs_trend) == "N/A":
+ pass
+ elif "-" in str(obs_trend):
+ station_obs_html += ' '
+ else:
+ station_obs_html += ' '
+ station_obs_html += ' ' # Close the span
+ station_obs_html += " "
+ station_obs_html += " "
+
+
+ """
+ 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
+ # Add to label array and strip whitespace if possible
+ if obs not in all_obs_unit_labels_json:
+ obs_unit_label = weewx.units.get_label_string(self.generator.formatter, self.generator.converter, obs)
+ 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 = """
+
+
+
+ """ % social_share_html
+ else:
+ facebook_html = ""
+
+ if twitter_enabled == "1":
+ twitter_html = """
+
+
+ """ % ( 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 = ''
+ # 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 += '
'
+ # Twitter second
+ if twitter_html != "":
+ social_html += twitter_html
+ social_html += "
"
+
+ """
+ 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,
+ 'radar_html_dark': radar_html_dark,
+ '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,
+ 'earthquake_distance_away': eqdistance,
+ 'earthquake_distance_label': eq_distance_label,
+ 'earthquake_bearing': eqbearing,
+ 'earthquake_bearing_raw': eqbearing_raw,
+ 'social_html': social_html,
+ 'custom_css_exists': custom_css_exists,
+ 'aqi': aqi,
+ 'aqi_category': aqi_category,
+ 'aqi_location': aqi_location,
+ 'beaufort0': beaufort0,
+ 'beaufort1': beaufort1,
+ 'beaufort2': beaufort2,
+ 'beaufort3': beaufort3,
+ 'beaufort4': beaufort4,
+ 'beaufort5': beaufort5,
+ 'beaufort6': beaufort6,
+ 'beaufort7': beaufort7,
+ 'beaufort8': beaufort8,
+ 'beaufort9': beaufort9,
+ 'beaufort10': beaufort10,
+ 'beaufort11': beaufort11,
+ 'beaufort12': beaufort12
+ }
+
+ # 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 has to be in milliseconds. Take the graphs.conf value and multiply by 1000
+ gapsize = plot_options.get('gapsize', 300000) # Default to 5 minutes in millis
+ if gapsize:
+ output[chart_group][plotname]["options"]["gapsize"] = gapsize * 1000
+
+ 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 legend option
+ legend = plot_options.get("legend", None)
+ if legend is None:
+ # Default to true if the option is missing
+ output[chart_group][plotname]["options"]["legend"] = "true"
+ else:
+ output[chart_group][plotname]["options"]["legend"] = legend
+
+ # 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
+ start_at_midnight = to_bool( line_options.get('start_at_midnight', False) ) # Should our timespan start at midnight?
+ 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 == "year_to_now":
+ minstamp, maxstamp = self.timespan_year_to_now( timespan.stop )
+ elif time_length == "hour_ago_to_now":
+ if start_at_midnight:
+ span_start, span_stop = archiveSpanSpan( timespan.stop, hour_delta=time_ago )
+ minstamp, maxstamp = TimeSpan( startOfDay( span_start ), span_stop )
+ else:
+ minstamp, maxstamp = archiveSpanSpan( timespan.stop, hour_delta=time_ago )
+ elif time_length == "day_ago_to_now":
+ if start_at_midnight:
+ span_start, span_stop = archiveSpanSpan( timespan.stop, day_delta=time_ago )
+ minstamp, maxstamp = TimeSpan( startOfDay( span_start ), span_stop )
+ else:
+ minstamp, maxstamp = archiveSpanSpan( timespan.stop, day_delta=time_ago )
+ elif time_length == "week_ago_to_now":
+ if start_at_midnight:
+ span_start, span_stop = archiveSpanSpan( timespan.stop, week_delta=time_ago )
+ minstamp, maxstamp = TimeSpan( startOfDay( span_start ), span_stop )
+ else:
+ minstamp, maxstamp = archiveSpanSpan( timespan.stop, week_delta=time_ago )
+ elif time_length == "month_ago_to_now":
+ if start_at_midnight:
+ span_start, span_stop = archiveSpanSpan( timespan.stop, month_delta=time_ago )
+ minstamp, maxstamp = TimeSpan( startOfDay( span_start ), span_stop )
+ else:
+ minstamp, maxstamp = archiveSpanSpan( timespan.stop, month_delta=time_ago )
+ elif time_length == "year_ago_to_now":
+ if start_at_midnight:
+ span_start, span_stop = archiveSpanSpan( timespan.stop, year_delta=time_ago )
+ minstamp, maxstamp = TimeSpan( startOfDay( span_start ), span_stop )
+ else:
+ minstamp, maxstamp = archiveSpanSpan( timespan.stop, year_delta=time_ago )
+ elif time_length == "timestamp_ago_to_now":
+ if start_at_midnight:
+ minstamp, maxstamp = TimeSpan( startOfDay( time_ago ), timespan.stop )
+ else:
+ minstamp, maxstamp = TimeSpan( time_ago, timespan.stop )
+ 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
+ if start_at_midnight:
+ span_start = plotgen_ts - time_length # Take the generation time and subtract the time_length to get our start time
+ minstamp = startOfDay( span_start )
+ else:
+ 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 and unit_label:
+ # Python 2/3 hack
+ try:
+ yAxis_label = yAxisLabel_config + " (" + unit_label.strip().encode("utf-8") + ")" # Python 2.
+ except:
+ yAxis_label = yAxisLabel_config + " (" + 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"
+
+ # Set default colors, unless the user has specified otherwise in graphs.conf
+ wind_rose_color = {}
+ wind_rose_color[0] = line_options.get('beauford0', "#7cb5ec")
+ wind_rose_color[1] = line_options.get('beauford1', "#b2df8a")
+ wind_rose_color[2] = line_options.get('beauford2', "#f7a35c")
+ wind_rose_color[3] = line_options.get('beauford3', "#8c6bb1")
+ wind_rose_color[4] = line_options.get('beauford4', "#dd3497")
+ wind_rose_color[5] = line_options.get('beauford5', "#e4d354")
+ wind_rose_color[6] = line_options.get('beauford6', "#268bd2")
+
+ # 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, wind_rose_color)
+
+ # 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, wind_rose_color):
+ """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] )
+ elif windSpeed_unit == "beaufort":
+ if windData[1] <= 1:
+ group_0_windDir.append( windData[0] )
+ group_0_windSpeed.append( windData[1] )
+ elif windData[1] == 2:
+ group_1_windDir.append( windData[0] )
+ group_1_windSpeed.append( windData[1] )
+ elif windData[1] == 3:
+ group_2_windDir.append( windData[0] )
+ group_2_windSpeed.append( windData[1] )
+ elif windData[1] == 4:
+ group_3_windDir.append( windData[0] )
+ group_3_windSpeed.append( windData[1] )
+ elif windData[1] == 5:
+ group_4_windDir.append( windData[0] )
+ group_4_windSpeed.append( windData[1] )
+ elif windData[1] == 6:
+ group_5_windDir.append( windData[0] )
+ group_5_windSpeed.append( windData[1] )
+ elif windData[1] >= 7:
+ 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+"
+ elif windSpeed_unit == "beaufort":
+ group_0_speedRange = "0"
+ group_1_speedRange = "1"
+ group_2_speedRange = "2"
+ group_3_speedRange = "3"
+ group_4_speedRange = "4"
+ group_5_speedRange = "5"
+ group_6_speedRange = "6+"
+
+ 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",
+ "color": wind_rose_color[0],
+ "zIndex": 106,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_0_series_data
+ }
+ group_1 = { "name": group_1_name,
+ "type": "column",
+ "color": wind_rose_color[1],
+ "zIndex": 105,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_1_series_data
+ }
+ group_2 = { "name": group_2_name,
+ "type": "column",
+ "color": wind_rose_color[2],
+ "zIndex": 104,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_2_series_data
+ }
+ group_3 = { "name": group_3_name,
+ "type": "column",
+ "color": wind_rose_color[3],
+ "zIndex": 103,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_3_series_data
+ }
+ group_4 = { "name": group_4_name,
+ "type": "column",
+ "color": wind_rose_color[4],
+ "zIndex": 102,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_4_series_data
+ }
+ group_5 = { "name": group_5_name,
+ "type": "column",
+ "color": wind_rose_color[5],
+ "zIndex": 101,
+ "stacking": "normal",
+ "fillOpacity": 0.75,
+ "data": group_5_series_data
+ }
+ group_6 = { "name": group_6_name,
+ "type": "column",
+ "color": wind_rose_color[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
+
+ if observation == "aqiChart":
+ data = { "aqiChart": True, "obsdata": [{'y': aqi, 'category': aqi_category}] }
+ return data
+
+ # Hays chart
+ if observation == "haysChart":
+
+ start_ts = int(start_ts)
+ end_ts = int(end_ts)
+
+ # Set aggregate interval based on timespan and make sure it is between 5 minutes and 1 day
+ logging.debug("Start time is %s and end time is %s" % (start_ts, end_ts))
+ aggregate_interval = (end_ts - start_ts)/360
+ if (aggregate_interval < 300):
+ aggregate_interval = 300
+ elif (aggregate_interval > 86400):
+ aggregate_interval = 86400
+ logging.debug("Interval is: %s" % aggregate_interval)
+
+ aggregate_type = "max"
+ # Get min values
+ obs_lookup = "windSpeed"
+ 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
+ obs_lookup = "windGust"
+ 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)
+
+ obs_unit = max_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])
+
+ data = {"haysChart": 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":
+ if isinstance(time_length, int):
+ sql_lookup = 'SELECT strftime("{0}", datetime(dateTime, "unixepoch", "localtime")) as {1}, IFNULL({2}({3}),0) as obs, dateTime FROM archive WHERE dateTime >= {4} AND dateTime <= {5} GROUP BY {6} ORDER BY dateTime ASC;'.format( strformat, xAxis_groupby, aggregate_type, obs_lookup, start_ts, end_ts, xAxis_groupby )
+ else:
+ 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":
+ if isinstance(time_length, int):
+ sql_lookup = 'SELECT FROM_UNIXTIME( dateTime, "%{0}" ) AS {1}, IFNULL({2}({3}),0) as obs, dateTime FROM archive WHERE dateTime >= {4} AND dateTime <= {5} GROUP BY {6} ORDER BY dateTime ASC;'.format( strformat, xAxis_groupby, aggregate_type, obs_lookup, start_ts, end_ts, xAxis_groupby )
+ else:
+ 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
+ # Do not keep adding None or empty results, so that full-length charts (like weewx v4 archiveYearSpan) don't have a line that continues past the last actual plot
+ obs_round_vt.append( rain )
+ continue
+ 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[1], "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 timespan_year_to_now(self, time_ts, grace=1, years_ago=0):
+ """In weewx 4 the get_series() for archiveYearSpan returns the full 365 day chart.
+ if users do not want a full year (with empty data) and would rather a Jan 1 to "now", then
+ they can use this custom timespan
+
+ This is taken right from weewx, but adapted to end at the current timestamp, and not the following Jan 1.
+ """
+ if time_ts is None:
+ return None
+ time_ts -= grace
+ _day_date = datetime.date.fromtimestamp(time_ts)
+ return TimeSpan(int(time.mktime((_day_date.year - years_ago, 1, 1, 0, 0, 0, 0, 0, -1))),
+ int(float(time_ts)))
+
+ 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
+
diff --git a/skins/Belchertown/header.html.tmpl b/skins/Belchertown/header.html.tmpl
index 4ae5c682..8bb09944 100644
--- a/skins/Belchertown/header.html.tmpl
+++ b/skins/Belchertown/header.html.tmpl
@@ -106,6 +106,7 @@
+
#if $page == "pi"
diff --git a/skins/Belchertown/images/null.png b/skins/Belchertown/images/null.png
new file mode 100644
index 00000000..075b94ee
Binary files /dev/null and b/skins/Belchertown/images/null.png differ
diff --git a/skins/Belchertown/index.html.tmpl b/skins/Belchertown/index.html.tmpl
index 72e6d56f..b6353434 100644
--- a/skins/Belchertown/index.html.tmpl
+++ b/skins/Belchertown/index.html.tmpl
@@ -1,4 +1,4 @@
-
+
#errorCatcher Echo
##
## Specifying an encoding of UTF-8 is usually safe, but if your text is
@@ -37,6 +37,11 @@
#end if
jQuery(document).ready(function() {
+ get_aqi_color( "$aqi" );
+ // weewx >= 4.2 can convert to Beaufort directly, but to improve backwards compatibility, convert windSpeed to
+ // knots and then use Javascript function to convert to Beaufort
+ jQuery(".beaufort").html( beaufort_cat( kts_to_beaufort( $current.windSpeed.knot.toString(addLabel=False) ) ) );
+
get_outTemp_color( "$unit.unit_type.outTemp", "$current.outTemp.formatted" );
rotateThis( "$current.windDir.formatted" );
@@ -206,6 +211,14 @@
$current_obs_summary
+ #if $Extras.has_key("aqi_enabled") and $Extras.aqi_enabled == '1'
+
+ AQI: $aqi ($aqi_category)
+ #if $Extras.has_key("aqi_location_enabled") and $Extras.aqi_location_enabled == '1'
+ $aqi_location
+ #end if
+
+ #end if
#if $current.appTemp.has_data
@@ -278,6 +291,11 @@
$unit.label.windSpeed
+ #if $Extras.has_key("beaufort_category") and $Extras.beaufort_category == '1'
+
+
+
+ #end if
@@ -319,7 +337,7 @@
#if $almanac.moon_index == 0
#else if $almanac.moon_index == 1
-
+
#else if $almanac.moon_index == 2
#else if $almanac.moon_index == 3
@@ -329,7 +347,7 @@
#else if $almanac.moon_index == 5
#else if $almanac.moon_index == 6
-
+
#else if $almanac.moon_index == 7
#end if
@@ -373,7 +391,7 @@
- $radar_html
+
$radar_html
@@ -417,7 +435,7 @@
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '24'
+ #if ($Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '24') or not ($Extras.has_key("forecast_interval_hours"))
#end if
diff --git a/skins/Belchertown/index.html.tmpl.mju b/skins/Belchertown/index.html.tmpl.mju
index 72e6d56f..b6353434 100644
--- a/skins/Belchertown/index.html.tmpl.mju
+++ b/skins/Belchertown/index.html.tmpl.mju
@@ -1,4 +1,4 @@
-
+
#errorCatcher Echo
##
## Specifying an encoding of UTF-8 is usually safe, but if your text is
@@ -37,6 +37,11 @@
#end if
jQuery(document).ready(function() {
+ get_aqi_color( "$aqi" );
+ // weewx >= 4.2 can convert to Beaufort directly, but to improve backwards compatibility, convert windSpeed to
+ // knots and then use Javascript function to convert to Beaufort
+ jQuery(".beaufort").html( beaufort_cat( kts_to_beaufort( $current.windSpeed.knot.toString(addLabel=False) ) ) );
+
get_outTemp_color( "$unit.unit_type.outTemp", "$current.outTemp.formatted" );
rotateThis( "$current.windDir.formatted" );
@@ -206,6 +211,14 @@
$current_obs_summary
+ #if $Extras.has_key("aqi_enabled") and $Extras.aqi_enabled == '1'
+
+ AQI: $aqi ($aqi_category)
+ #if $Extras.has_key("aqi_location_enabled") and $Extras.aqi_location_enabled == '1'
+ $aqi_location
+ #end if
+
+ #end if
#if $current.appTemp.has_data
@@ -278,6 +291,11 @@
$unit.label.windSpeed
+ #if $Extras.has_key("beaufort_category") and $Extras.beaufort_category == '1'
+
+
+
+ #end if
@@ -319,7 +337,7 @@
#if $almanac.moon_index == 0
#else if $almanac.moon_index == 1
-
+
#else if $almanac.moon_index == 2
#else if $almanac.moon_index == 3
@@ -329,7 +347,7 @@
#else if $almanac.moon_index == 5
#else if $almanac.moon_index == 6
-
+
#else if $almanac.moon_index == 7
#end if
@@ -373,7 +391,7 @@
- $radar_html
+
$radar_html
@@ -417,7 +435,7 @@
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '24'
+ #if ($Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '24') or not ($Extras.has_key("forecast_interval_hours"))
#end if
diff --git a/skins/Belchertown/index.html.tmpl.nju b/skins/Belchertown/index.html.tmpl.nju
deleted file mode 100644
index 72e6d56f..00000000
--- a/skins/Belchertown/index.html.tmpl.nju
+++ /dev/null
@@ -1,559 +0,0 @@
-
-#errorCatcher Echo
-##
-## Specifying an encoding of UTF-8 is usually safe, but if your text is
-## actually in Latin-1, then you should replace the string "UTF-8" with "latin-1"
-## If you do this, you should also change the 'Content-Type' metadata below.
-#encoding UTF-8
-##
-#set global $page = "home"
-
- #include "header.html.tmpl"
-
-
-
-
-
-
-
-
-
-
-
-
$obs.label.home_page_header
-
-
- $obs.label.powered_by
-
-
-
-
- #if $social_html != ""
- $social_html
- #end if
-
-
- #if $Extras.has_key("forecast_alert_enabled") and $Extras.forecast_alert_enabled == '1'
-
- #end if
-
-
-
-
-
-
-
-
-
-
-
- #if $Extras.has_key("forecast_enabled") and $Extras.forecast_enabled == '1' and $current_obs_icon != ""
-
- #end if
-
-
-
$current.outTemp.formatted $unit.label.outTemp
-
-
-
-
-
- $current_obs_summary
-
-
-
- #if $current.appTemp.has_data
-
$obs.label.feels_like: $current.appTemp
- #end if
-
-
-
-
- $obs.label.highest_temperature
- $obs.label.lowest_temperature
-
-
- $day.outTemp.max
- $day.outTemp.min
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #if $current.windDir.ordinal_compass == "N/A"
- --
- #else
- $current.windDir.ordinal_compass
- #end if
-
-
- #if $current.windDir.raw is None:
- -
- #else
- $current.windDir.format("%.0f")
- #end if
-
-
-
-
-
-
-
-
-
- $obs.label.wind_speed
- $obs.label.wind_gust
-
-
-
-
- $current.windSpeed.toString(addLabel=False, NONE_string="--")
-
-
-
-
- $current.windGust.toString(addLabel=False, NONE_string="--")
-
-
-
-
- $unit.label.windSpeed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $station_obs_html
-
-
-
-
-
-
-
-
-
-
-
- #if $obs.label.sun_and_moon and $obs.label.sun_and_moon != ''
-
- $obs.label.sun_and_moon
-
- #end if
-
-
-
-
-
-
-
-
- #if $almanac.moon_index == 0
-
- #else if $almanac.moon_index == 1
-
- #else if $almanac.moon_index == 2
-
- #else if $almanac.moon_index == 3
-
- #else if $almanac.moon_index == 4
-
- #else if $almanac.moon_index == 5
-
- #else if $almanac.moon_index == 6
-
- #else if $almanac.moon_index == 7
-
- #end if
-
-
#echo $almanac.moon_phase.title()#
-
-
$almanac.moon_fullness% $obs.label.moon_visible
-
-
-
- #if $Extras.has_key("almanac_extras") and $Extras.almanac_extras == '1' and $almanac.hasExtras
-
-
-
-
-
-
-
-
- #include "celestial.inc"
-
-
-
-
-
- #end if
-
-
-
-
-
-
-
-
-
-
- $radar_html
-
-
-
-
- #if os.path.exists("index_hook_after_station_info.inc")
-
-
- #include "index_hook_after_station_info.inc"
-
-
- #end if
-
- #if $Extras.has_key("forecast_enabled") and $Extras.forecast_enabled == '1' and (($Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours != '0') or not ($Extras.has_key("forecast_interval_hours")))
-
-
-
-
- $obs.label.forecast_header
-
-
-
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours != '0'
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '1'
-
- #end if
-
- #end if
-
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '3'
-
- #end if
-
-
- #if $Extras.has_key("forecast_interval_hours") and $Extras.forecast_interval_hours == '24'
-
- #end if
-
-
-
-
- #end if
-
-
-
- #if os.path.exists("index_hook_after_forecast.inc")
-
-
- #include "index_hook_after_forecast.inc"
-
-
- #end if
-
-
-
- #if $Extras.has_key('earthquake_enabled') and $Extras.earthquake_enabled == '1'
-
- #else
-
- #end if
-
-
-
-
- $obs.label.weather_snapshots
$obs.label.weather_snapshots_link
-
-
-
-
-
-
-
-
- $obs.label.snapshot_high: $day.outTemp.max
- $obs.label.snapshot_low: $day.outTemp.min
-
-
- $obs.label.snapshot_today_avg_wind: $day.wind.avg
- $obs.label.snapshot_today_high_wind: $day.wind.max
-
-
- $obs.label.snapshot_today_rain: $day.rain.sum
- $obs.label.snapshot_today_rainrate: $day.rainRate.max
-
-
-
-
-
-
-
-
-
-
-
- $obs.label.snapshot_high: $month.outTemp.max
- $obs.label.snapshot_low: $month.outTemp.min
-
-
- $obs.label.snapshot_month_avg_wind: $month.wind.avg
- $obs.label.snapshot_month_high_wind: $month.wind.max
-
-
- $obs.label.snapshot_month_rain: $month.rain.sum
- $obs.label.snapshot_month_rainrate: $month.rainRate.max
-
-
-
-
-
-
- #if $Extras.has_key('earthquake_enabled') and $Extras.earthquake_enabled == '1'
-
-
-
$obs.label.earthquake_title
-
-
- #if $earthquake_place != ''
-
-
$earthquake_place
-
-
- $obs.label.earthquake_magnitude $earthquake_magnitude
-
-
- $earthquake_distance_away $earthquake_distance_label $earthquake_bearing
-
-
- #end if
-
-
- #end if
-
-
-
- #if os.path.exists("index_hook_after_snapshot.inc")
-
-
- #include "index_hook_after_snapshot.inc"
-
-
- #end if
-
- #if $Extras.has_key('highcharts_enabled') and $Extras.highcharts_enabled == '1'
-
-
-
- #end if
-
-
-
- #if os.path.exists("index_hook_after_charts.inc")
-
-
- #include "index_hook_after_charts.inc"
-
-
- #end if
-
-
-
-
-
-
- #include "footer.html.tmpl"
diff --git a/skins/Belchertown/js/belchertown.js.tmpl b/skins/Belchertown/js/belchertown.js.tmpl
index de7dbcd5..2cce8301 100644
--- a/skins/Belchertown/js/belchertown.js.tmpl
+++ b/skins/Belchertown/js/belchertown.js.tmpl
@@ -1,4 +1,4 @@
-// mju version - Nov 2020
+// mju version Jan 2021
#encoding UTF-8
#import datetime
@@ -198,6 +198,190 @@ function get_outTemp_color( unit, outTemp, returnColor=false ) {
}
}
+// Change the color of the aqi variable according to US-EPA standards
+// (adjusted to match skin colors better)
+function get_aqi_color( aqi, returnColor=false ) {
+ if ( aqi >= 301 ) {
+ var aqi_color = "#cc241d";
+ } else if ( aqi >= 201 ) {
+ var aqi_color = "#b16286";
+ } else if ( aqi >= 151 ) {
+ var aqi_color = "rgba(255,69,69,1)";
+ } else if ( aqi >= 101 ) {
+ var aqi_color = "rgba(255,127,0,1)";
+ } else if ( aqi >= 51 ) {
+ var aqi_color = "rgba(255,174,0,0.9)";
+ } else if ( aqi < 51 ) {
+ var aqi_color = "#71bc3c";
+ }
+
+ // Return the color value if requested, otherwise just set the div color
+ if ( returnColor ) {
+ return aqi_color;
+ } else {
+ jQuery(".aqi_outer").css( "color", aqi_color );
+ }
+}
+
+function get_gauge_color( value, options ) {
+ if (options.color1) {
+ // Failsafe in case value drops below the lowest color position user has set.
+ // Otherwise color is undefined when the value is below color1_position
+ var color = options.color1
+ }
+ if (options.color2) {
+ if (value >= options.color2_position) {
+ var color = options.color2
+ }
+ }
+ if (options.color3) {
+ if (value >= options.color3_position) {
+ var color = options.color3
+ }
+ }
+ if (options.color4) {
+ if (value >= options.color4_position) {
+ var color = options.color4
+ }
+ }
+ if (options.color5) {
+ if (value >= options.color5_position) {
+ var color = options.color5
+ }
+ }
+ if (options.color6) {
+ if (value >= options.color6_position) {
+ var color = options.color6
+ }
+ }
+ if (options.color7) {
+ if (value >= options.color7_position){
+ var color = options.color7
+ }
+ }
+ return color
+}
+
+function get_gauge_label( value, options ) {
+ if (options.color1) {
+ if (options.color1_label) {
+ var label = options.color1_label
+ }
+ }
+ if (options.color2) {
+ if (value >= options.color2_position) {
+ var label = null
+ if (options.color2_label) {
+ var label = options.color2_label
+ }
+ }
+ }
+ if (options.color3) {
+ if (value >= options.color3_position) {
+ var label = null
+ if (options.color3_label) {
+ var label = options.color3_label
+ }
+ }
+ }
+ if (options.color4) {
+ if (value >= options.color4_position) {
+ var label = null
+ if (options.color4_label) {
+ var label = options.color4_label
+ }
+ }
+ }
+ if (options.color5) {
+ if (value >= options.color5_position) {
+ var label = null
+ if (options.color5_label) {
+ var label = options.color5_label
+ }
+ }
+ }
+ if (options.color6) {
+ if (value >= options.color6_position) {
+ var label = null
+ if (options.color6_label) {
+ var label = options.color6_label
+ }
+ }
+ }
+ if (options.color7) {
+ if (value >= options.color7_position){
+ var label = null
+ if (options.color7_label) {
+ var label = options.color7_label
+ }
+ }
+ }
+ return label
+}
+
+function kts_to_beaufort(windspeed) {
+ // Given windspeed in knots, converts to Beaufort scale
+ if (windspeed <= 1) {
+ return 0
+ } else if (windspeed <= 3) {
+ return 1
+ } else if (windspeed <=6) {
+ return 2
+ } else if (windspeed <=10) {
+ return 3
+ } else if (windspeed <=15) {
+ return 4
+ } else if (windspeed <=21) {
+ return 5
+ } else if (windspeed <=27) {
+ return 6
+ } else if (windspeed <=33) {
+ return 7
+ } else if (windspeed <=40) {
+ return 8
+ } else if (windspeed <=47) {
+ return 9
+ } else if (windspeed <=55) {
+ return 10
+ } else if (windspeed <=63) {
+ return 11
+ } else if (windspeed > 63) {
+ return 12
+ }
+}
+
+function beaufort_cat(beaufort) {
+ // Given Beaufort number, returns category description
+ switch (beaufort) {
+ case 0:
+ return "$beaufort0"
+ case 1:
+ return "$beaufort1"
+ case 2:
+ return "$beaufort2"
+ case 3:
+ return "$beaufort3"
+ case 4:
+ return "$beaufort4"
+ case 5:
+ return "$beaufort5"
+ case 6:
+ return "$beaufort6"
+ case 7:
+ return "$beaufort7"
+ case 8:
+ return "$beaufort8"
+ case 9:
+ return "$beaufort9"
+ case 10:
+ return "$beaufort10"
+ case 11:
+ return "$beaufort11"
+ case 12:
+ return "$beaufort12"
+ }
+}
+
function highcharts_tooltip_factory(obsvalue, point_obsType, highchartsReturn=false, rounding, mirrored=false, numberFormat) {
// Mirrored values have the negative sign removed
if ( mirrored ) {
@@ -333,31 +517,23 @@ function autoTheme(sunset_hour, sunset_min, sunrise_hour, sunrise_min) {
belchertown_debug("Auto theme: are we in daylight hours: " + dayTime);
belchertown_debug("Auto theme: sessionStorage.getItem('theme') = " + sessionStorage.getItem('theme'));
- if ( dayTime ) {
+ if ( dayTime == true ) {
// Day time, set light if needed
- if ( document.body.classList.contains("dark") ) {
- if ( sessionStorage.getItem('theme') == "auto" ) {
- belchertown_debug("Auto theme: setting light theme since dayTime variable is true (day)");
- } else {
- belchertown_debug("Auto theme: cannot set light theme since visitor used toggle to override theme. Refresh to reset the override.");
- }
- // Only change theme if user has not overridden the auto option with the toggle
- if ( sessionStorage.getItem('theme') == "auto" ) {
- changeTheme("light");
- }
+ // Only change theme if user has not overridden the auto option with the toggle
+ if ( sessionStorage.getItem('theme') == "auto" ) {
+ belchertown_debug("Auto theme: setting light theme since dayTime variable is true (day)");
+ changeTheme("light");
+ } else {
+ belchertown_debug("Auto theme: cannot set light theme since visitor used toggle to override theme. Refresh to reset the override.");
}
} else {
// Night time, set dark if needed
- if ( document.body.classList.contains("light") ) {
- if ( sessionStorage.getItem('theme') == "auto" ) {
- belchertown_debug("Auto theme: setting dark theme since dayTime variable is false (night)");
- } else {
- belchertown_debug("Auto theme: cannot set dark theme since visitor used toggle to override theme. Refresh to reset the override.");
- }
- // Only change theme if user has not overridden the auto option with the toggle
- if ( sessionStorage.getItem('theme') == "auto" ) {
- changeTheme("dark");
- }
+ // Only change theme if user has not overridden the auto option with the toggle
+ if ( sessionStorage.getItem('theme') == "auto" ) {
+ belchertown_debug("Auto theme: setting dark theme since dayTime variable is false (night)");
+ changeTheme("dark");
+ } else {
+ belchertown_debug("Auto theme: cannot set dark theme since visitor used toggle to override theme. Refresh to reset the override.");
}
}
}
@@ -375,6 +551,9 @@ function changeTheme(themeName, toggleOverride=false) {
}
if ( themeName == "dark" ) {
// Apply dark theme
+ #if $radar_html_dark != "None"
+ jQuery('.radar_image').html( '$radar_html_dark' );
+ #end if
jQuery('body').addClass("dark");
jQuery('body').removeClass("light");
#if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1'
@@ -387,6 +566,9 @@ function changeTheme(themeName, toggleOverride=false) {
sessionStorage.setItem('currentTheme', 'dark');
} else if ( themeName == "light" ) {
// Apply light theme
+ #if $radar_html_dark != "None"
+ jQuery('.radar_image').html( '$radar_html' );
+ #end if
jQuery('body').addClass("light");
jQuery('body').removeClass("dark");
#if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1'
@@ -625,6 +807,7 @@ function aeris_coded_alerts( data, full_observation = false ) {
"CF.A": "$obs.label.forecast_alert_code_CF_A",
"FG.Y": "$obs.label.forecast_alert_code_FG_Y",
"MF.Y": "$obs.label.forecast_alert_code_MF_Y",
+ "FO.Y": "$obs.label.forecast_alert_code_FO_Y",
"SM.Y": "$obs.label.forecast_alert_code_SM_Y",
"MS.Y": "$obs.label.forecast_alert_code_MS_Y",
"DS.W": "$obs.label.forecast_alert_code_DS_W",
@@ -894,7 +1077,7 @@ function aeris_icon( data ) {
"showers": "rain",
"showersn": "rain",
"showersw": "rain",
- "showersw": "rain",
+ "showerswn": "rain",
"sleet": "sleet",
"sleetn": "sleet",
"sleetsnow": "sleet",
@@ -1001,8 +1184,7 @@ function update_forecast_data( data ) {
forecast_provider = "$Extras.forecast_provider";
belchertown_debug("Forecast: Provider is " + forecast_provider);
belchertown_debug("Forecast: Updating data");
- belchertown_debug(data);
-
+ belchertown_debug(data);
if ( forecast_provider == "N/A" ) {
jQuery(".forecastrow").hide();
@@ -1032,7 +1214,7 @@ function update_forecast_data( data ) {
} catch(err) {
// Visibility not in the station observation table or any of the unit arrays, so silently exit
}
-
+
// start of new composite version of forecast code
var forecast_types = ["forecast_1hr","forecast_3hr","forecast_24hr"];
@@ -1077,7 +1259,13 @@ function update_forecast_data( data ) {
}
// Determine wind units
- if ( "$Extras.forecast_units" == "ca" ) {
+ if ( "$unit.unit_type.windSpeed" == "knot" ) {
+ windSpeed = data[(forecast_interval)][0]["response"][0]["periods"][i]["windSpeedKTS"];
+ windGust = data[(forecast_interval)][0]["response"][0]["periods"][i]["windGustKTS"];
+ } else if ( "$unit.unit_type.windSpeed" == "beaufort" ) {
+ windSpeed = kts_to_beaufort(data[(forecast_interval)][0]["response"][0]["periods"][i]["windSpeedKTS"]);
+ windGust = kts_to_beaufort(data[(forecast_interval)][0]["response"][0]["periods"][i]["windGustKTS"]);
+ } else if ( "$Extras.forecast_units" == "ca" ) {
// ca = kph
windSpeed = data[(forecast_interval)][0]["response"][0]["periods"][i]["windSpeedKPH"];
windGust = data[(forecast_interval)][0]["response"][0]["periods"][i]["windGustKPH"];
@@ -1102,7 +1290,7 @@ function update_forecast_data( data ) {
}
// ensure that precip is always defined
precip = 0;
- } else if ( data[(forecast_interval)][0]["response"][0]["periods"][i]["pop"] > 0 ) {
+ } else if ( data[(forecast_interval)][0]["response"][0]["periods"][i]["pop"] > 0 ) {
// Rain percent of precip
precip = data[(forecast_interval)][0]["response"][0]["periods"][i]["pop"];
} else {
@@ -1169,6 +1357,7 @@ function update_forecast_data( data ) {
output_html += '';
if ( forecast_row[i]["snow_depth"] > 0 ) {
output_html += '
';
+ // output_html += '
';
output_html += ' '+ parseFloat( forecast_row[i]["snow_depth"] ).toFixed(0) +' ' + forecast_row[i]["snow_unit"];
output_html += ' ';
} else if ( forecast_row[i]["precip"] > 0 ) {
@@ -1598,6 +1787,16 @@ function update_current_wx(data) {
if ( data.hasOwnProperty("windSpeed_mps") ) {
jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_mps"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
}
+ // Windspeed Beaufort
+ if ( data.hasOwnProperty("windSpeed_beaufort") ) {
+ jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_beaufort"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
+ }
+ // Windspeed knots
+ if ( data.hasOwnProperty("windSpeed_knot") ) {
+ jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_knot"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
+ }
+
+ jQuery(".beaufort").html( beaufort_cat( parseFloat(data["beaufort_count"])) );
// Wind Gust US
// May not be provided in mqtt, but just in case.
@@ -1612,6 +1811,14 @@ function update_current_wx(data) {
if ( data.hasOwnProperty("windGust_mps") ) {
jQuery(".curwindgust").html( parseFloat(parseFloat(data["windGust_mps"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]}) );
}
+ // Wind Gust Beaufort
+ if ( data.hasOwnProperty("windGust_beaufort") ) {
+ jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windGust_beaufort"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
+ }
+ // Windspeed knots
+ if ( data.hasOwnProperty("windGust_knot") ) {
+ jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windGust_knot"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
+ }
// Windchill US
if ( data.hasOwnProperty("windchill") ) {
@@ -1631,6 +1838,10 @@ Highcharts.setOptions({
timezoneOffset: $highcharts_timezoneoffset
},
lang: {
+ months: moment.months(),
+ shortMonths: moment.monthsShort(),
+ weekdays: moment.weekdays(),
+ shortWeekdays: moment.weekdaysShort(),
decimalPoint: "$highcharts_decimal",
thousandsSep: "$highcharts_thousands"
}
@@ -2048,10 +2259,6 @@ function showChart(json_file, prepend_renderTo=false) {
// Barometer chart plots get a higher precision yAxis tick
if (s.obsType == "barometer") {
- if ( typeof s.yAxis_tickInterval === "undefined" ) {
- // If no tick interval override, set 0.01 as default tick interval to satisfy an old request for this level of precision.
- options.yAxis[this_yAxis].tickInterval = 0.01;
- }
// Define yAxis label float format if rounding is defined. Default to 2 decimals if nothing defined
if ( typeof s.rounding !== "undefined" ) {
options.yAxis[this_yAxis].labels = { format: '{value:.'+s.rounding+'f}' }
@@ -2152,7 +2359,138 @@ function showChart(json_file, prepend_renderTo=false) {
options.series.push(ns);
});
}
-
+
+ // Configure gauge chart formatting
+ if (options.chart.type == "gauge") {
+ // Highcharts does not allow the guage background to have rounded ends. To get around
+ // this, define a "dummy" series that fills the gauge with the appropriate color.
+ // This way, the ends are rounded if the user specifies.
+ //
+ // Gauge chart works best with only one data point, so the most recent (last) data point
+ // is used
+ options.series[0].data = [{
+ y: 9999999,
+ color: '#e6e6e6',
+ className: 'highcharts-pane',
+ zIndex: 0,
+ dataLabels: {enabled: false}
+ }, {
+ y: options.series[0].data.pop()[1],
+ color: options.series[0].color,
+ }]
+ options.chart.type = "solidgauge"
+ options.pane = {
+ startAngle: -140,
+ endAngle: 140,
+ background: [{
+ outerRadius: 0,
+ innerRadius: 0,
+ }]
+ }
+ // If user has set colors_enabled, change the color according to the value
+ if (options.series[0].colors_enabled) {
+ options.series[0].data[1].color = get_gauge_color(options.series[0].data[1]["y"], options.series[0])
+ }
+ options.plotOptions = {
+ solidgauge: {
+ dataLabels: {
+ useHTML: true,
+ enabled: true,
+ borderWidth: 0,
+ style: {
+ fontWeight: 'bold',
+ lineHeight: '0.5em',
+ textAlign: 'center',
+ fontSize: '50px',
+ // Match color if set by user
+ color: options.series[0].data[1].color,
+ textOutline: 'none'
+ }
+ },
+ }
+ }
+ if ( get_gauge_label( options.series[0].data[1]["y"], options.series[0] ) ) {
+ options.plotOptions.solidgauge.dataLabels.format = "
{y:.#f} " + get_gauge_label( options.series[0].data[1]["y"], options.series[0] ) + ' '
+ options.plotOptions.solidgauge.dataLabels.y = -25
+ } else if ( unit_label_array[observation_type] == null ) {
+ options.plotOptions.solidgauge.dataLabels.format = "
{y:.#f} "
+ } else {
+ options.plotOptions.solidgauge.dataLabels.format = "
{y:.#f} " + unit_label_array[observation_type] + ' '
+ options.plotOptions.solidgauge.dataLabels.y = -25
+ }
+ options.yAxis = {
+ min: 0,
+ max: 100,
+ lineColor: null,
+ tickPositions: []
+ }
+ // Override default max and min if user has specified
+ if (options.series[0].yAxis_max) {
+ options.yAxis.max = options.series[0].yAxis_max;
+ }
+ if (options.series[0].yAxis_min) {
+ options.yAxis.min = options.series[0].yAxis_min;
+ }
+ options.tooltip.enabled = false
+ options.xAxis.crosshair = false
+ }
+
+ // If AQI chart is present, configure a special chart
+ if (observation_type == "aqiChart") {
+ // Highcharts does not allow the guage background to have rounded ends. To get around
+ // this, define a "dummy" series that fills the gauge with the appropriate color.
+ // This way, the ends are rounded if the user specifies.
+ options.series[0].data = [{
+ y: 500,
+ color: '#e6e6e6',
+ className: 'highcharts-pane',
+ zIndex: 0,
+ dataLabels: {enabled: false}
+ }, {
+ y: options.series[0].data[0]['y'],
+ color: get_aqi_color(options.series[0].data[0]['y'], true),
+ category: options.series[0].data[0]['category']
+ }]
+ options.chart.type = "solidgauge"
+ options.pane = {
+ startAngle: -140,
+ endAngle: 140,
+ background: [{
+ outerRadius: 0,
+ innerRadius: 0,
+ }]
+ }
+ options.plotOptions = {
+ solidgauge: {
+ dataLabels: {
+ useHTML: true,
+ enabled: true,
+ y: -30,
+ borderWidth: 0,
+ format: '
{y} ' + options.series[0].data[1]['category'] + ' ',
+ style: {
+ fontWeight: 'bold',
+ lineHeight: '0.5em',
+ textAlign: 'center',
+ fontSize: '50px',
+ color: options.series[0].data[1].color,
+ textOutline: 'none'
+ }
+ },
+ linecap: 'round',
+ rounded: true
+ }
+ }
+ options.yAxis = {
+ min: 0,
+ max: 500,
+ lineColor: null,
+ tickPositions: []
+ }
+ options.tooltip.enabled = false
+ options.xAxis.crosshair = false
+ }
+
// If Hays chart is present, configure a special chart to show that data
if (observation_type == "haysChart") {
options.chart.type = "arearange"
@@ -2165,20 +2503,16 @@ function showChart(json_file, prepend_renderTo=false) {
}
}
};
-
// Find min and max of the series data for the yAxis min and max
var maximum_flattened = [];
options.series[0].data.forEach( seriesData => {
maximum_flattened.push(seriesData[2]);
});
var range_max = Math.max(...maximum_flattened);
-
if (options.series[0].yAxis_softMax) {
var range_max = options.series[0].yAxis_softMax;
}
-
options.legend = { "enabled": false }
-
options.yAxis = {
showFirstLabel: false,
tickInterval: 2,
@@ -2194,7 +2528,6 @@ function showChart(json_file, prepend_renderTo=false) {
y: 0
},
}
-
options.tooltip = {
split: false,
shared: true,
@@ -2209,7 +2542,6 @@ function showChart(json_file, prepend_renderTo=false) {
});
}
}
-
var currentSeries = options.series;
var currentSeriesData = options.series[0].data;
var range_unit = options.series[0].range_unit;
@@ -2269,12 +2601,12 @@ function showChart(json_file, prepend_renderTo=false) {
},
}
- options.xAxis = {}
options.xAxis = {
- labels: {
- format: "{value: %b}"
+ dateTimeLabelFormats: {
+ day: '%e %b',
+ week: '%e %b',
+ month: '%b %y',
},
- tickInterval: 2592000000, // 30 days
showLastLabel: true,
crosshair: true,
type: "datetime"
diff --git a/skins/Belchertown/skin.README.txt b/skins/Belchertown/skin.README.txt
deleted file mode 100644
index ab75f10f..00000000
--- a/skins/Belchertown/skin.README.txt
+++ /dev/null
@@ -1,91 +0,0 @@
-Testing Belchertown Reports
-===========================
-
-There are two reasons for using a testing arangement
-
-1 To test new versions of the Belchertown skin after updates have been made before release to Lordship Weather
-2 Whilst developing a version of the Lordship Weather website. Can be used with care for any modifications.
-
-
-Using and Switching Belchertown test skin On & Off
-==================================================
-
-1 Check skin_blank exists in Belchertown/test folder
-2 Check skin_test.conf exists in the Belchertown/test folder
-3 Edits are made to skin.conf in BelchertownTest folder when it is set to skin_test.conf
-
-ON To turn the test report generation ON:
- 1 copy skin_test.conf to skin.conf for editing and testing
-OFF To turn the test report generation OFF:
- 1 copy skin.conf to skin_test.conf, preserving the changes that have been made during testing
- 2 copy skin_blank.conf to skin.conf, to ensure the blank is used
-
-
-IMPORTANT NOTE
-==============
-
-Watch for the generate tags. They have been commented out for testing but need to be replaced for live operation.
-Probably the best way of moving test to live is to do a compare on the test and live .conf files
-and then move the significant differences from test to live
-
-Also necessary to force the complete regeneration of all files because of the generate tags (daily, weekly, monthly etc)
-
-
-
-
-Setup
-=====
-
-Weewx - weewx.conf Changes
-==========================
-
-A Belchertown test setup, containing .conf files etc is set up in /etc/weewx/skins/Belchertown/test
-The weewx config file /etc/weex/weewx.conf is modified to contain a new stanza to produce reports in /var/weewx/reports/test
-
- [[BelchertownTest]]
- skin = BelchertownTest
- HTML_ROOT = /var/weewx/reports/test
- belchertown_root_url = /var/weewx/reports/test
- [[[Units]]]
- [[[[Groups]]]]
- group_altitude = meter
- group_speed2 = mile_per_hour2
- group_pressure = mbar
- group_rain = mm
- group_rainrate = mm_per_hour
- group_temperature = degree_C
- group_degree_day = degree_C_day
- group_speed = mile_per_hour
- [[[[TimeFormats]]]]
- current = %x %H:%M hrs
- [[[Extras]]]
- highcharts_homepage_graphgroup = homepage
- site_title = Lordship Weather
- footer_copyright_text = Lordship Weather
- forecast_enabled = 1
-
-BelchertownTest Changes
-=======================
-
-The contents of /etc/weewx/skins/Belechertown are copied to etc/weewx/skins/BelchertownTest
-A backup of skin.conf in /etc/weewx/BelchertownTest called skin_test.conf
-A new skin.conf (skin_blank.conf) is produced in /etc/weewx/skins/BelchertownTest with no contents
-This is then switched with skin_test.conf in the same folder to switch
-
-Using and Switching Belchertown test skin On & Off
-==================================================
-
-1 Check skin_blank exists in Belchertown/test folder
-2 Check skin_test.conf exists in the Belchertown/test folder
-3 Edits are made to skin.conf in BelchertownTest folder when it is set to skin_test.conf
-
-ON To turn the test report generation ON:
- 1 copy skin_test.conf to skin.conf for editing and testing
-OFF To turn the test report generation OFF:
- 1 copy skin.conf to skin_test.conf, preserving the changes that have been made during testing
- 2 copy skin_blank.conf to skin.conf, to ensure the blank is used
-
-
-
-
-
diff --git a/skins/Belchertown/skin.conf b/skins/Belchertown/skin.conf
index d297911f..24f7983c 100644
--- a/skins/Belchertown/skin.conf
+++ b/skins/Belchertown/skin.conf
@@ -2,7 +2,7 @@
# SKIN CONFIGURATION FILE #
# Copyright (c) 2010 Tom Keffer
#
# Updated for the Belchertown Skin by Pat O'Brien, 2019 #
-# Updated for Lordship Weather by Michael Underwood (# mju)- Aug/Nov 2020 #
+# Updated for Lordship Weather by Michael Underwood (# mju)- Jan 2021 #
###############################################################################
[Extras]
@@ -12,12 +12,12 @@
belchertown_locale = "auto"
theme = dark # mju - normally dark, but set to light for test site
theme_toggle_enabled = 1
- site_title = "Lordship Test Site" # mju - to distinguish test from live site
+ site_title = "Lordship Weather"
logo_image = ""
logo_image_dark = ""
#radar_html = ""
almanac_extras = 1
-
+ beaufort_category = 1
# mju - embedded windy.com inserted here to over-write the default, subsequently dropped in favour of meteo forecast
# mju - windy.com rain/thunder radar_windy = ''
@@ -76,24 +76,25 @@
# Forecast defaults
forecast_enabled = 1 # mju
forecast_provider = "aeris"
- forecast_api_id = ""
- forecast_api_secret = ""
+ forecast_api_id = "" # mju - transferred to weewx.conf
+ forecast_api_secret = "" # mju - transferred to weewx.conf
forecast_interval_hours = 1 # new values are 0,1,3 or 24
forecast_units = "uk2" # mju
forecast_lang = "en"
forecast_stale = 3540
forecast_aeris_use_metar = 1 # mju change to 0, after metar test mods carried out in belchertown.py (#393)
- forecast_alert_enabled = 1 # mju, now that aeris does european alerts
+ forecast_alert_enabled = 0 # mju, now that uk no longer appears to be part of eumetnet post brexit
forecast_alert_limit = 1
forecast_show_daily_forecast_link = 0
forecast_daily_forecast_link = ""
-
+ aqi_enabled = 1
+ aqi_location_enabled = 1
# Earthquake defaults
earthquake_enabled = 1
earthquake_maxradiuskm = 1000
earthquake_stale = 10740
earthquake_server = USGS
-
+ geonet_mmi = 4
# Social Share Button Defaults. Define the text below under Labels
facebook_enabled = 0
twitter_enabled = 0
@@ -233,6 +234,14 @@
forecast_last_updated = Last Updated on
forecast_interval_caption = Forecast Interval (hours):
+ # Air Quality Index label defaults
+ aqi_good = good
+ aqi_moderate = moderate
+ aqi_usg = unhealthy for some # Official wording: "unhealthy for sensitive groups"
+ aqi_unhealthy = unhealthy
+ aqi_very_unhealthy = very unhealthy
+ aqi_hazardous = hazardous
+
# Aeris Weather Forecast Codes. From https://www.aerisweather.com/support/docs/api/reference/weather-codes/
forecast_cloud_code_CL = "Clear"
forecast_cloud_code_FW = "Mostly Clear"
@@ -318,6 +327,7 @@
forecast_alert_code_CF_A = "Coastal Flood Watch"
forecast_alert_code_FG_Y = "Dense Fog Advisory"
forecast_alert_code_MF_Y = "Dense Fog Advisory"
+ forecast_alert_code_FO_Y = "Fog Advisory"
forecast_alert_code_SM_Y = "Dense Smoke Advisory"
forecast_alert_code_MS_Y = "Dense Smoke Advisory"
forecast_alert_code_DS_W = "Dust Storm Warning"
diff --git a/skins/Belchertown/skin.conf.mju b/skins/Belchertown/skin.conf.mju
index d297911f..24f7983c 100644
--- a/skins/Belchertown/skin.conf.mju
+++ b/skins/Belchertown/skin.conf.mju
@@ -2,7 +2,7 @@
# SKIN CONFIGURATION FILE #
# Copyright (c) 2010 Tom Keffer #
# Updated for the Belchertown Skin by Pat O'Brien, 2019 #
-# Updated for Lordship Weather by Michael Underwood (# mju)- Aug/Nov 2020 #
+# Updated for Lordship Weather by Michael Underwood (# mju)- Jan 2021 #
###############################################################################
[Extras]
@@ -12,12 +12,12 @@
belchertown_locale = "auto"
theme = dark # mju - normally dark, but set to light for test site
theme_toggle_enabled = 1
- site_title = "Lordship Test Site" # mju - to distinguish test from live site
+ site_title = "Lordship Weather"
logo_image = ""
logo_image_dark = ""
#radar_html = ""
almanac_extras = 1
-
+ beaufort_category = 1
# mju - embedded windy.com inserted here to over-write the default, subsequently dropped in favour of meteo forecast
# mju - windy.com rain/thunder radar_windy = ''
@@ -76,24 +76,25 @@
# Forecast defaults
forecast_enabled = 1 # mju
forecast_provider = "aeris"
- forecast_api_id = ""
- forecast_api_secret = ""
+ forecast_api_id = "" # mju - transferred to weewx.conf
+ forecast_api_secret = "" # mju - transferred to weewx.conf
forecast_interval_hours = 1 # new values are 0,1,3 or 24
forecast_units = "uk2" # mju
forecast_lang = "en"
forecast_stale = 3540
forecast_aeris_use_metar = 1 # mju change to 0, after metar test mods carried out in belchertown.py (#393)
- forecast_alert_enabled = 1 # mju, now that aeris does european alerts
+ forecast_alert_enabled = 0 # mju, now that uk no longer appears to be part of eumetnet post brexit
forecast_alert_limit = 1
forecast_show_daily_forecast_link = 0
forecast_daily_forecast_link = ""
-
+ aqi_enabled = 1
+ aqi_location_enabled = 1
# Earthquake defaults
earthquake_enabled = 1
earthquake_maxradiuskm = 1000
earthquake_stale = 10740
earthquake_server = USGS
-
+ geonet_mmi = 4
# Social Share Button Defaults. Define the text below under Labels
facebook_enabled = 0
twitter_enabled = 0
@@ -233,6 +234,14 @@
forecast_last_updated = Last Updated on
forecast_interval_caption = Forecast Interval (hours):
+ # Air Quality Index label defaults
+ aqi_good = good
+ aqi_moderate = moderate
+ aqi_usg = unhealthy for some # Official wording: "unhealthy for sensitive groups"
+ aqi_unhealthy = unhealthy
+ aqi_very_unhealthy = very unhealthy
+ aqi_hazardous = hazardous
+
# Aeris Weather Forecast Codes. From https://www.aerisweather.com/support/docs/api/reference/weather-codes/
forecast_cloud_code_CL = "Clear"
forecast_cloud_code_FW = "Mostly Clear"
@@ -318,6 +327,7 @@
forecast_alert_code_CF_A = "Coastal Flood Watch"
forecast_alert_code_FG_Y = "Dense Fog Advisory"
forecast_alert_code_MF_Y = "Dense Fog Advisory"
+ forecast_alert_code_FO_Y = "Fog Advisory"
forecast_alert_code_SM_Y = "Dense Smoke Advisory"
forecast_alert_code_MS_Y = "Dense Smoke Advisory"
forecast_alert_code_DS_W = "Dust Storm Warning"
diff --git a/skins/Belchertown/skin_blank.conf b/skins/Belchertown/skin_blank.conf
deleted file mode 100644
index a64aa7e7..00000000
--- a/skins/Belchertown/skin_blank.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-###############################################################################
-# SKIN CONFIGURATION FILE #
-# Copyright (c) 2010 Tom Keffer #
-# Updated for the Belchertown Skin by Pat O'Brien, 2019 #
-# Updated for Lordship Weather by Michael Underwood - June 2020 #
-# Converted into a blank skin to switch Belchertowntest on/off #
-# without having to change parameters in weewx.conf and restarting it #
-###############################################################################
-
diff --git a/skins/Belchertown/skin_test.conf b/skins/Belchertown/skin_test.conf
deleted file mode 100644
index a06a2da8..00000000
--- a/skins/Belchertown/skin_test.conf
+++ /dev/null
@@ -1,588 +0,0 @@
-###############################################################################
-# SKIN CONFIGURATION FILE #
-# Copyright (c) 2010 Tom Keffer #
-# Updated for the Belchertown Skin by Pat O'Brien, 2019 #
-# Updated for Lordship Weather by Michael Underwood (# mju)- August 2020 #
-###############################################################################
-
-[Extras]
-
- # General Site Defaults
- belchertown_debug = 0
- belchertown_locale = "auto"
- theme = light # mju - normally dark, but set to light for test site
- theme_toggle_enabled = 1
- site_title = "Lordship Test Site" # mju - to distinguish test from live site
- logo_image = ""
- logo_image_dark = ""
- #radar_html = ""
- almanac_extras = 1
-
- # mju - embedded windy.com inserted here to over-write the default, subsequently dropped in favour of meteo forecast
-
- # mju - windy.com rain/thunder radar_windy = ''
-
- # mju - meteoradar.co.uk - used so that rainfall radar forecast (verwacht) shows on Nexus tablet, actual = actueel
-
- radar_html = ''
-
- # Station Observations. Special observation rainWithRainRate combines Daily Rain with Rain Rate in 1 line
- station_observations = "barometer", "dewpoint", "outHumidity", "rainWithRainRate", "radiation", "UV", "inTemp"
-
- # Manifest Settings for Mobile Phones
- manifest_name = "Lordship Weather" # mju - was "My Weather Website"
- manifest_short_name = "LWX" # mju - was "MWW"
-
- # Highcharts settings
- highcharts_enabled = 1
- graph_page_show_all_button = 1
- graph_page_default_graphgroup = "day"
- highcharts_homepage_graphgroup = "homepage"
- highcharts_decimal = "auto"
- highcharts_thousands = "auto"
-
- # MQTT Websockets defaults
- mqtt_websockets_enabled = 0
- mqtt_websockets_host = ""
- mqtt_websockets_port = 1883
- mqtt_websockets_ssl = 0
- mqtt_websockets_topic = ""
- disconnect_live_website_visitor = 1800000
-
- # Show an alert if the page updated timestamp is older than expected with this setting. Does not apply to MQTT Websocket enabled websites
- # The late time threshold is defined in seconds. This should be greater than your archive_interval from weewx.conf.
- # Typically you would want this 2 or 3 times archive_interval
- show_last_updated_alert = 0
- last_updated_alert_threshold = 1800
-
- # If mqtt_websockets_enabled is set to 0, but want the page to full reload on an interval, specify this below in milliseconds. 300000 = 5 minutes
- webpage_autorefresh = 300000 # mju - was 0
-
- # Image Reload Section.
- # Set reload_hook_images to 1 to enable, then set the number of *seconds* for each section to reload.
- # A value of -1 will disable reloading images in that section.
- # radar = the radar image if you used radar_html setting
- # asi = index_hook_after_station_info.inc
- # af = index_hook_after_forecast.inc
- # as = index_hook_after_snapshot.inc
- # ac = index_hook_after_charts.inc
- reload_hook_images = 0
- reload_images_radar = 300
- reload_images_hook_asi = -1
- reload_images_hook_af = -1
- reload_images_hook_as = -1
- reload_images_hook_ac = -1
-
- # Forecast defaults
- forecast_enabled = 1 # mju
- forecast_3hr_enabled = 1 # new
- forecast_1hr_enabled = 1 # new
- forecast_provider = "aeris"
- forecast_api_id = "4GQJ42R3uZi9oTwsYT5rI" # mju
- forecast_api_secret = "PWoDWZg3VExO26Pfr39FwOtmsFtBy8E5kGRmUUXQ" # mju
- forecast_interval_hours = 3 # new values are 0,1,3 or 24; defaults to 24
- forecast_units = "uk2" # mju
- forecast_lang = "en"
- forecast_stale = 3540
- forecast_alert_enabled = 1 # mju, now that aeris does european alerts
- forecast_alert_limit = 1
- forecast_show_daily_forecast_link = 0
- forecast_daily_forecast_link = ""
-
- # Earthquake defaults
- earthquake_enabled = 1
- earthquake_maxradiuskm = 1000
- earthquake_stale = 10740
-
- # Social Share Button Defaults. Define the text below under Labels
- facebook_enabled = 0
- twitter_enabled = 0
- social_share_html = "http://yourwebsite"
-
- # Google Analytics
- #googleAnalyticsId = UA-12345678-1
-
- # This is the display of the Pi Kiosk which is in the /pi folder
- pi_kiosk_bold = "false"
- pi_theme = "auto"
-
-###############################################################################
-
-[Labels]
- # Labels used in this skin
-
- [[Generic]]
- # Generic labels, keyed by an observation type.
- # To change a label or translate it to your language
- # change the text after the equal sign.
-
- # Extra Observation labels
- appTemp = Apparent Temperature
- cloudbase = Cloud Base
- visibility = Visibility
- windrun = Wind Run
-
- # HTML Header Meta Tags and HTML Title. These labels have a default value
- # set inside of header.html.tmpl. Leave as "" to use the default value.
- html_title = ""
- html_description = ""
-
- # Footer Information
- footer_copyright_text = "Lordship Weather" # mju - was "My Weather Website"
- footer_disclaimer_text = "Use data from this website at your own risk"
-
- # Twitter Social Share
- twitter_text = "Check out my website: My Weather Website Weather Conditions"
- twitter_owner = "YourTwitterUsernameHere"
- twitter_hashtags = "weewx #weather"
-
- # Station Observation Table
- rainWithRainRate = Rain
-
- # Navigation Menu
- nav_home = Home
- nav_graphs = Graphs
- nav_records = Records
- nav_reports = Reports
- nav_about = About
-
- # Default page headers
- home_page_header = "Letchworth South-East Weather Conditions" # mju - was "My Station
- graphs_page_header = "Weather Observation Graphs"
- records_page_header = "Weather Observation Records"
- reports_page_header = "Weather Observation Reports"
- about_page_header = "About This Weather Station"
- powered_by = "Data from an Aercus WeatherSleuth" # mju
-
- # Earthquake translations
- earthquake_no_data = No recent earthquake data available!
-
- # Home Page Text and Titles
- second = "second", "seconds"
- minute = "minute", "minutes"
- hour = "hour", "hours"
- sun = Sun
- moon = Moon
- sun_and_moon = Sun & Moon
- moon_visible = visible
- wind_speed = Speed
- wind_gust = Gust
- wind_today_max = Today Max
- feels_like = Feels like
- highest_temperature = High
- lowest_temperature = Low
- average_temperature = Average
- header_last_updated_alert = Notice: This page hasn't been updated recently and may contain stale data!
- header_last_updated = Last Updated
- mqtt_websockets_connecting = Connecting to weather station real time data.
- mqtt_websockets_waiting = Connected. Waiting for data.
- mqtt_websockets_connected = Connected to weather station live. Data received
- mqtt_websockets_stopped = Live updates have stopped.
- mqtt_websockets_continue = Continue live updates
- mqtt_websockets_failed = Failed connecting to the weather station. Please try again later!
- mqtt_websockets_lost = Lost connection to the weather station. Please try again later!
- weather_snapshots = Weather Record Snapshots.
- weather_snapshots_link = View all weather records here.
- snapshot_high = High
- snapshot_low = Low
- snapshot_today_avg_wind = Average Wind
- snapshot_today_high_wind = Highest Wind
- snapshot_today_high_uv = Highest UV
- snapshot_today_rain = Today's Rain
- snapshot_today_rainrate = Highest Rate
- snapshot_month_avg_wind = Average Wind
- snapshot_month_high_wind = Highest Wind
- snapshot_month_high_uv = Highest UV
- snapshot_month_rain = Total Rain
- snapshot_month_rainrate = Highest Rate
- earthquake_title = Recent Local Earthquake
- earthquake_magnitude = Magnitude
- homepage_graphs_link = View more here.
- copyright = Copyright
-
- # Almanac Popup
- close = Close
- almanac_more_details = More Almanac Information
- almanac_modal_title = Almanac Information
- sun_always_down = Always down
- sun_always_up = Always up
- more_than_yesterday = more than yesterday
- less_than_yesterday = less than yesterday
- start_civil_twilight = Start civil twilight
- rise = Rise
- transit = Transit
- set = Set
- end_civil_twilight = End Civil Twilight
- azimuth = Azimuth
- altitude = Altitude
- right_ascension = Right ascension
- declination = Declination
- equinox = Equinox
- solstice = Solstice
- total_daylight = Total daylight
- full_moon = Full moon
- new_moon = New moon
- phase = Phase
- full = full
- install_pyephem = Install pyephem for detailed celestial timings.
-
- # General Forecast translations
- forecast_header = Daily Forecast #(Powered by AerisWeather ) # mju to meet aeris t&c
- forecast_1hr_header = Hourly Forecast #(Openweather ) # new for openweather
- forecast_3hr_header = 3-hourly Forecast #(Powered by AerisWeather ) # mju to meet aeris t&c
- daily_forecast = Daily Forecast (Powered by AerisWeather ) # mju to meet aeris t&c; not used
- alert_in_effect = in effect until
- forecast_last_updated = Last Updated on
-
- # Aeris Weather Forecast Codes. From https://www.aerisweather.com/support/docs/api/reference/weather-codes/
- forecast_cloud_code_CL = "Clear"
- forecast_cloud_code_FW = "Mostly Clear"
- forecast_cloud_code_SC = "Partly Cloudy"
- forecast_cloud_code_BK = "Mostly Cloudy"
- forecast_cloud_code_OV = "Cloudy"
-
- forecast_coverage_code_AR = "Areas of"
- forecast_coverage_code_BR = "Brief"
- forecast_coverage_code_C = "Chance of"
- forecast_coverage_code_D = "Definite"
- forecast_coverage_code_FQ = "Frequent"
- forecast_coverage_code_IN = "Intermittent"
- forecast_coverage_code_IS = "Isolated"
- forecast_coverage_code_L = "Likely"
- forecast_coverage_code_NM = "Numerous"
- forecast_coverage_code_O = "Occasional"
- forecast_coverage_code_PA = "Patchy"
- forecast_coverage_code_PD = "Periods of"
- forecast_coverage_code_S = "Slight Chance of"
- forecast_coverage_code_SC = "Scattered"
- forecast_coverage_code_VC = "In the Vicinity"
- forecast_coverage_code_WD = "Widespread"
-
- forecast_intensity_code_VL = "Very Light"
- forecast_intensity_code_L = "Light"
- forecast_intensity_code_H = "Heavy"
- forecast_intensity_code_VH = "Very Heavy"
-
- forecast_weather_code_A = "Hail"
- forecast_weather_code_BD = "Blowing Dust"
- forecast_weather_code_BN = "Blowing Sand"
- forecast_weather_code_BR = "Mist"
- forecast_weather_code_BS = "Blowing Snow"
- forecast_weather_code_BY = "Blowing Spray"
- forecast_weather_code_F = "Fog"
- forecast_weather_code_FR = "Frost"
- forecast_weather_code_H = "Haze"
- forecast_weather_code_IC = "Ice Crystals"
- forecast_weather_code_IF = "Ice Fog"
- forecast_weather_code_IP = "Sleet"
- forecast_weather_code_K = "Smoke"
- forecast_weather_code_L = "Drizzle"
- forecast_weather_code_R = "Rain"
- forecast_weather_code_RW = "Rain Showers"
- forecast_weather_code_RS = "Rain/Snow Mix"
- forecast_weather_code_SI = "Snow/Sleet Mix"
- forecast_weather_code_WM = "Wintry Mix"
- forecast_weather_code_S = "Snow"
- forecast_weather_code_SW = "Snow Showers"
- forecast_weather_code_T = "Thunderstorms"
- forecast_weather_code_UP = "Unknown Precipitation"
- forecast_weather_code_VA = "Volcanic Ash"
- forecast_weather_code_WP = "Waterspouts"
- forecast_weather_code_ZF = "Freezing Fog"
- forecast_weather_code_ZL = "Freezing Drizzle"
- forecast_weather_code_ZR = "Freezing Rain"
- forecast_weather_code_ZY = "Freezing Spray"
-
- # DarkSky Specific Codes
- forecast_weather_code_W = "Windy"
- forecast_weather_code_TO = "Tornado"
-
- # Graphs Page Text and Titles
- graphs_page_all_button = All
- graphs_windrose_frequency = Frequency
- graphs_windDir_ordinals = '{ 0: "N", 90: "E", 180: "S", 270: "W", 360: "N" }'
-
- # Records Page Text and Titles
- records_ending = ending
- records_days_text = days
- records_all_time = All Time
- records_temperature_records = Temperature Records
- records_high_temp = Highest Temperature
- records_low_temp = Lowest Temperature
- records_high_apptemp = Highest Apparent Temperature
- records_low_apptemp = Lowest Apparent Temperature
- records_high_heatindex = Highest Heat Index
- records_low_windchill = Lowest Wind Chill
- records_largest_temp_range = Largest Daily Temperature Range
- records_smallest_temp_range = Smallest Daily Temperature Range
- records_wind_records = Wind Records
- records_strongest_wind = Strongest Wind Gust
- records_daily_windrun = Highest Daily Wind Run
- records_rain_records = Rain Records
- records_highest_daily_rainfall = Highest Daily Rainfall
- records_highest_daily_rainrate = Highest Daily Rain Rate
- records_month_high_rainfall = Month with Highest Total Rainfall
- records_total_rainfall = Total Rainfall for
- records_consec_days_with_rain = Consecutive Days With Rain
- records_consec_days_without_rain = Consecutive Days Without Rain
- records_humidity_records = Humidity Records
- records_high_humidity = Highest Humidity
- records_lowest_humidity = Lowest Humidity
- records_highest_dewpoint = Highest Dewpoint
- records_lowest_dewpoint = Lowest Dewpoint
- records_barometer_records = Barometer Records
- records_high_barometer = Highest Barometer
- records_low_barometer = Lowest Barometer
- records_sun_records = Sun Records
- records_high_solar_rad = Highest Solar Radiation
- records_high_uv = Highest UV
- records_inTemp_records = Inside Temp Records
- records_high_inTemp = Highest inside Temp
- records_low_inTemp = Lowest inside Temp
-
- # NOAA Reports Page Text and Titles
- reports_title = NOAA Reports
- reports_click_here_link = Click here
- reports_view_more = to view this report directly or click on a month or year to change the NOAA report.
-
- # Pi Page Text and Titles
- mqtt_websockets_waiting_pi = Connecting.
- mqtt_websockets_connected_pi = Connected. Received
-
- # moment.js default labels formats
- time_earthquake = "LLL"
- time_last_updated = "LL, LTS"
- time_snapshot_records_today_header = "dddd, LL"
- time_snapshot_records_month_header = "MMMM YYYY"
- time_sunrise = "LT"
- time_sunset = "LT"
- time_forecast_alert_expires = "LLL"
- time_forecast_date = "ddd D MMM" # mju - has to be set explicitly as default is US format
- time_forecast_time = "ddd HH:mm" # mju - has to be set explicitly as default is US format
- time_forecast_last_updated = "LLL"
- time_records_page_full_date = "LLL"
- time_records_page_month_day_year = "LL"
- time_records_page_rainfall_range_begin = "MMMM DD"
- time_records_page_rainfall_range_end = "LL"
-
-
-
-###############################################################################
-
-[Almanac]
- # The labels to be used for the phases of the moon:
- moon_phases = New Moon, Waxing Crescent, First Quarter, Waxing Gibbous, Full Moon, Waning Gibbous, Last Quarter, Waning Crescent
-
-###############################################################################
-
-[Units]
- # This section is for managing the selection and formatting of units.
-
- [[Groups]]
- # For each group of measurements, this section sets what units to
- # use for it.
- # NB: The unit is always in the singular. I.e., 'mile_per_hour',
- # NOT 'miles_per_hour'
-
- group_altitude = foot # Options are 'foot' or 'meter'
- group_degree_day = degree_F_day # Options are 'degree_F_day' or 'degree_C_day'
- group_direction = degree_compass
- group_distance = mile # Options are 'mile' or 'km'
- group_moisture = centibar
- group_percent = percent
- group_pressure = inHg # Options are 'inHg', 'mmHg', 'mbar', or 'hPa'
- group_radiation = watt_per_meter_squared
- group_rain = inch # Options are 'inch', 'cm', or 'mm'
- group_rainrate = inch_per_hour # Options are 'inch_per_hour', 'cm_per_hour', or 'mm_per_hour'
- group_speed = mile_per_hour # Options are 'mile_per_hour', 'km_per_hour', 'knot', or 'meter_per_second'
- group_speed2 = mile_per_hour2 # Options are 'mile_per_hour2', 'km_per_hour2', 'knot2', or 'meter_per_second2'
- group_temperature = degree_F # Options are 'degree_F' or 'degree_C'
- group_uv = uv_index
- group_volt = volt
-
- # The following are used internally and should not be changed:
- group_count = count
- group_interval = minute
- group_time = unix_epoch
- group_elapsed = second
-
- [[StringFormats]]
- # This section sets the string formatting for each type of unit.
-
- centibar = %.0f
- cm = %.2f
- cm_per_hour = %.2f
- degree_C = %.1f
- degree_F = %.1f
- degree_compass = %.0f
- foot = %.0f
- hPa = %.1f
- hour = %.1f
- inHg = %.3f
- inch = %.2f
- inch_per_hour = %.2f
- km = %.2f
- km_per_hour = %.0f
- km_per_hour2 = %.1f
- knot = %.0f
- knot2 = %.1f
- mbar = %.1f
- meter = %.0f
- meter_per_second = %.1f
- meter_per_second2 = %.1f
- mile = %.2f
- mile_per_hour = %.0f
- mile_per_hour2 = %.1f
- mm = %.1f
- mmHg = %.1f
- mm_per_hour = %.1f
- percent = %.0f
- second = %.0f
- uv_index = %.1f
- volt = %.1f
- watt_per_meter_squared = %.0f
- NONE = "N/A"
-
- [[Labels]]
- # This section sets a label to be used for each type of unit.
-
- centibar = " cb"
- cm = " cm"
- cm_per_hour = " cm/hr"
- degree_C = " °C"
- degree_F = " °F"
- degree_compass = °
- foot = " feet"
- hPa = " hPa"
- inHg = " inHg"
- inch = " in"
- inch_per_hour = " in/hr"
- km = " km"
- km_per_hour = " km/h"
- km_per_hour2 = " km/h"
- knot = " knots"
- knot2 = " knots"
- mbar = " mbar"
- meter = " meters"
- meter_per_second = " m/s"
- meter_per_second2 = " m/s"
- mile = " miles"
- mile_per_hour = " mph"
- mile_per_hour2 = " mph"
- mm = " mm"
- mmHg = " mmHg"
- mm_per_hour = " mm/hr"
- percent = %
- volt = " V"
- watt_per_meter_squared = " W/m²"
- day = " day", " days"
- hour = " hour", " hours"
- minute = " minute", " minutes"
- second = " second", " seconds"
- NONE = ""
-
- [[TimeFormats]]
- # This section sets the string format to be used for each time scale.
- # The values below will work in every locale, but may not look
- # particularly attractive. See the Customization Guide for alternatives.
-
- day = %X
- week = %X (%A)
- month = %x %X
- year = %x %X
- rainyear = %x %X
- current = %x %X
- ephem_day = %X
- ephem_year = %x %X
-
- [[Ordinates]]
- # The ordinal directions. The last one should be for no wind direction
- directions = N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW, N/A
-
- [[DegreeDays]]
- # This section sets the base temperatures used for the calculation
- # of heating and cooling degree-days.
-
- # Base temperature for heating days, with unit:
- heating_base = 65, degree_F
- # Base temperature for cooling days, with unit:
- cooling_base = 65, degree_F
-
- [[Trend]]
- time_delta = 10800 # 3 hours
- time_grace = 300 # 5 minutes
-
-###############################################################################
-
-[CheetahGenerator]
- # This section is used by the generator CheetahGenerator, and specifies
- # which files are to be generated from which template.
-
- search_list_extensions = user.belchertown.getData
-
- # Possible encodings are 'html_entities', 'utf8', or 'strict_ascii'
- encoding = html_entities
-
- [[SummaryByMonth]]
- # Reports that summarize "by month"
- [[[NOAA_month]]]
- encoding = strict_ascii
- template = NOAA/NOAA-YYYY-MM.txt.tmpl
-
- [[SummaryByYear]]
- # Reports that summarize "by year"
- [[[NOAA_year]]]
- encoding = strict_ascii
- template = NOAA/NOAA-YYYY.txt.tmpl
-
- [[ToDate]]
- # Reports that show statistics "to date", such as day-to-date,
- # week-to-date, month-to-date, etc.
- [[[weewx_data]]]
- template = json/weewx_data.json.tmpl
-
- [[Belchertown]]
- template = js/belchertown.js.tmpl
-
- [[[home]]]
- template = index.html.tmpl
-
- [[[about]]]
- template = about/index.html.tmpl
-
- [[[graphs]]]
- template = graphs/index.html.tmpl
-
- [[[records]]]
- template = records/index.html.tmpl
-
- [[[reports]]]
- template = reports/index.html.tmpl
-
- [[[pi]]]
- template = pi/index.html.tmpl
-
- [[[manifest]]]
- encoding = utf8
- template = manifest.json.tmpl
-
-###############################################################################
-
-[CopyGenerator]
-
- # This section is used by the generator CopyGenerator
-
- # List of files to be copied only the first time the generator runs
- copy_once = favicon.ico, images/*, json/index.html, js/index.html, js/responsive-menu.js, robots.txt
-
- # List of files to be copied each time the generator runs
- copy_always = *.css
-
-
-###############################################################################
-
-#
-# The list of generators that are to be run:
-#
-[Generators]
- generator_list = weewx.cheetahgenerator.CheetahGenerator, weewx.reportengine.CopyGenerator, user.belchertown.HighchartsJsonGenerator
diff --git a/skins/Belchertown/style.css b/skins/Belchertown/style.css
index 9e463872..4901bc01 100644
--- a/skins/Belchertown/style.css
+++ b/skins/Belchertown/style.css
@@ -1,4 +1,4 @@
-/* mju version - Nov 2020 */
+/* mju version - Jan 2021 */
/*
CSS file for Belchertown weather website.
This website was originally created in WordPress on Genesis WP.
@@ -1464,6 +1464,16 @@ p.entry-meta {
font-size:20px;
}
+.aqi_outer {
+ text-align:center;
+ font-size:20px;
+}
+
+.aqi_location_outer {
+ text-align:center;
+ font-size:14px;
+}
+
#wxicon {
display: block;
margin: 0 auto;
@@ -1641,8 +1651,12 @@ p.entry-meta {
}
.mph {
- padding-top:10px !important;
- font-size:18px !important;
+ padding-top: 5px !important;
+ font-size: 15px !important;
+}
+
+.beaufort {
+ padding-top: 10px !important;
}
.wx-pressure-trend {
diff --git a/skins/Belchertown/style.css.mju b/skins/Belchertown/style.css.mju
index 9e463872..4901bc01 100644
--- a/skins/Belchertown/style.css.mju
+++ b/skins/Belchertown/style.css.mju
@@ -1,4 +1,4 @@
-/* mju version - Nov 2020 */
+/* mju version - Jan 2021 */
/*
CSS file for Belchertown weather website.
This website was originally created in WordPress on Genesis WP.
@@ -1464,6 +1464,16 @@ p.entry-meta {
font-size:20px;
}
+.aqi_outer {
+ text-align:center;
+ font-size:20px;
+}
+
+.aqi_location_outer {
+ text-align:center;
+ font-size:14px;
+}
+
#wxicon {
display: block;
margin: 0 auto;
@@ -1641,8 +1651,12 @@ p.entry-meta {
}
.mph {
- padding-top:10px !important;
- font-size:18px !important;
+ padding-top: 5px !important;
+ font-size: 15px !important;
+}
+
+.beaufort {
+ padding-top: 10px !important;
}
.wx-pressure-trend {