#encoding UTF-8 #import datetime #if "-" in str($station.stn_info.latitude_f) #set global $hemisphere = "southern-hemisphere" #else #set global $hemisphere = "northern-hemisphere" #end if var pages = ["graphs", "records", "reports", "about", "pi"]; var pageName = ""; // If this page we're on now is listed as a subpage, use ".." to get to the relative root function get_relative_url() { var sPath = window.location.pathname.replace(/\/$/, ""); pageName = sPath.substring(sPath.lastIndexOf('/') + 1); if ( pages.includes( pageName ) ) { var relative_url = ".."; } else { var relative_url = "."; } belchertown_debug("URL: Relative URL is: " + relative_url); return relative_url; } // Determine if debug is on via URL var or config setting if ( getURLvar("debug") && ( getURLvar("debug") == "true" || getURLvar("debug") == "1" ) ) { var belchertown_debug_config = true; belchertown_debug("Debug: URL debug variable enabled"); } else { var belchertown_debug_config = $belchertown_debug; belchertown_debug("Debug: skin.conf belchertown_debug enabled"); } var moment_locale = "$system_locale_js"; moment.locale(moment_locale); var graphgroups_raw = $charts; var graphgroups_titles = $graphpage_titles; var graphpage_content = $graphpage_content; function belchertown_debug(message) { if (belchertown_debug_config > 0) { console.log(message); } } jQuery(document).ready(function() { // Bootstrap hover tooltips jQuery(function () { jQuery('[data-toggle="tooltip"]').tooltip() }) // If the visitor has overridden the theme, keep that theme going throughout the full site and their visit. if ( sessionStorage.getItem('theme') == "toggleOverride" ) { belchertown_debug("Theme: sessionStorage override in place."); changeTheme( sessionStorage.getItem('currentTheme') ); } // Change theme if a URL variable is set if ( window.location.search.indexOf('theme') ) { if (window.location.search.indexOf('?theme=dark') === 0) { belchertown_debug("Theme: Setting dark theme because of URL override"); changeTheme("dark", true); } else if (window.location.search.indexOf('?theme=light') === 0) { belchertown_debug("Theme: Setting light theme because of URL override"); changeTheme("light", true); } else if (window.location.search.indexOf('?theme=auto') === 0) { belchertown_debug("Theme: Setting auto theme because of URL override"); sessionStorage.setItem('theme', 'auto') #if $almanac.sunrise.raw is not None and $almanac.sunset.raw is not None autoTheme(#echo datetime.datetime.fromtimestamp($almanac.sunset.raw).strftime('%H')#, #echo datetime.datetime.fromtimestamp($almanac.sunset.raw).strftime('%M')#, #echo datetime.datetime.fromtimestamp($almanac.sunrise.raw).strftime('%H')#, #echo datetime.datetime.fromtimestamp($almanac.sunrise.raw).strftime('%M')#) #end if } } #if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1' // Dark mode checkbox toggle switcher try { document.getElementById('themeSwitch').addEventListener('change', function(event) { belchertown_debug("Theme: Toggle button changed"); (event.target.checked) ? changeTheme("dark", true) : changeTheme("light", true); }); } catch(err) { // Silently exit } #end if // After charts are loaded, if an anchor tag is in the URL, let's scroll to it jQuery(window).on('load', function() { var anchor_tag = location.hash.replace('#',''); if ( anchor_tag != '' ) { // Scroll the webpage to the chart. The timeout is to let jQuery finish appending the outer div so the height of the page is completed. setTimeout(function() { jQuery('html, body').animate({ scrollTop: jQuery('#'+anchor_tag).offset().top }, 500); }, 500); } }); }); #if $Extras.has_key("theme") and $Extras.theme == 'auto' // Run this on every page for dark mode if skin theme is auto ajaxweewx(); #end if // Disable AJAX caching jQuery.ajaxSetup({ cache:false }); // Get the URL variables. Source: https://stackoverflow.com/a/26744533/1177153 function getURLvar(k) { var p={}; location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){p[k]=v}); return k?p[k]:p; } // http://stackoverflow.com/a/14887961/1177153 var weatherdirection = $obs.label.graphs_windDir_ordinals; // Change the color of the outTemp_F variable function get_outTemp_color( unit, outTemp, returnColor=false ) { outTemp = parseFloat( outTemp ).toFixed(0); // Convert back to decimal literal if ( unit == "degree_F" ) { if ( outTemp <= 0 ) { var outTemp_color = "#1278c8"; } else if ( outTemp <= 25 ) { var outTemp_color = "#30bfef"; } else if ( outTemp <= 32 ) { var outTemp_color = "#1fafdd"; } else if ( outTemp <= 40 ) { var outTemp_color = "rgba(0,172,223,1)"; } else if ( outTemp <= 50 ) { var outTemp_color = "#71bc3c"; } else if ( outTemp <= 55 ) { var outTemp_color = "rgba(90,179,41,0.8)"; } else if ( outTemp <= 65 ) { var outTemp_color = "rgba(131,173,45,1)"; } else if ( outTemp <= 70 ) { var outTemp_color = "rgba(206,184,98,1)"; } else if ( outTemp <= 75 ) { var outTemp_color = "rgba(255,174,0,0.9)"; } else if ( outTemp <= 80 ) { var outTemp_color = "rgba(255,153,0,0.9)"; } else if ( outTemp <= 85 ) { var outTemp_color = "rgba(255,127,0,1)"; } else if ( outTemp <= 90 ) { var outTemp_color = "rgba(255,79,0,0.9)"; } else if ( outTemp <= 95 ) { var outTemp_color = "rgba(255,69,69,1)"; } else if ( outTemp <= 110 ) { var outTemp_color = "rgba(255,104,104,1)"; } else if ( outTemp >= 111 ) { var outTemp_color = "rgba(218,113,113,1)"; } } else if ( unit == "degree_C" ) { if ( outTemp <= 0 ) { var outTemp_color = "#1278c8"; } else if ( outTemp <= -3.8 ) { var outTemp_color = "#30bfef"; } else if ( outTemp <= 0 ) { var outTemp_color = "#1fafdd"; } else if ( outTemp <= 4.4 ) { var outTemp_color = "rgba(0,172,223,1)"; } else if ( outTemp <= 10 ) { var outTemp_color = "#71bc3c"; } else if ( outTemp <= 12.7 ) { var outTemp_color = "rgba(90,179,41,0.8)"; } else if ( outTemp <= 18.3 ) { var outTemp_color = "rgba(131,173,45,1)"; } else if ( outTemp <= 21.1 ) { var outTemp_color = "rgba(206,184,98,1)"; } else if ( outTemp <= 23.8 ) { var outTemp_color = "rgba(255,174,0,0.9)"; } else if ( outTemp <= 26.6 ) { var outTemp_color = "rgba(255,153,0,0.9)"; } else if ( outTemp <= 29.4 ) { var outTemp_color = "rgba(255,127,0,1)"; } else if ( outTemp <= 32.2 ) { var outTemp_color = "rgba(255,79,0,0.9)"; } else if ( outTemp <= 35 ) { var outTemp_color = "rgba(255,69,69,1)"; } else if ( outTemp <= 43.3 ) { var outTemp_color = "rgba(255,104,104,1)"; } else if ( outTemp >= 43.4 ) { var outTemp_color = "rgba(218,113,113,1)"; } } // Return the color value if requested, otherwise just set the div color if ( returnColor ) { return outTemp_color; } else { jQuery(".outtemp_outer").css( "color", outTemp_color ); } } // Change the color of the Windspeed variable according to US-EPA standards // (adjusted to match skin colors better) function get_windSpeed_color(windSpeed, returnColor = false) { if (windSpeed <= 0) { var windSpeed_color = "rgba(0,255,0,1)"; } else if (windSpeed >= 19.9) { var windSpeed_color = "rgba(126,255,0,1)"; } else if (windSpeed >= 39.9) { var windSpeed_color = "rgba(0,255,0,1)"; } else if (windSpeed >= 59.9) { var windSpeed_color = "rgba(255,128,0,1)"; } else if (windSpeed >= 79.9) { var windSpeed_color = "rgba(255,0,0,1)"; } else if (windSpeed >= 99.9) { var windSpeed_color = "rgba(255,0,120,1)"; } else if (windSpeed >= 118.0) { var windSpeed_color = "rgba(255,1,113,1)"; } // Return the color value if requested, otherwise just set the div color if (returnColor) { return windSpeed_color; } else { jQuery(".curwindspeed").css("color", windSpeed_color); } } // Change the color of the aqi variable according to US-EPA standards // (adjusted to match skin colors better) function get_windGust_color(windGust, returnColor = false) { if (windGust <= 0) { var windGust_color = "rgba(0,255,0,1)"; } else if (windGust >= 19.9) { var windGust_color = "rgba(126,255,0,1)"; } else if (windGust >= 39.9) { var windGust_color = "rgba(0,255,0,1)"; } else if (windGust >= 59.9) { var windGust_color = "rgba(255,128,0,1)"; } else if (windGust >= 79.9) { var windGust_color = "rgba(255,0,0,1)"; } else if (windGust >= 99.9) { var windGust_color = "rgba(255,0,120,1)"; } else if (windGust >= 118.0) { var windGust_color = "rgba(255,1,113,1)"; } // Return the color value if requested, otherwise just set the div color if (returnColor) { return windGust_color; } else { jQuery(".curwindgust").css("color", windGust_color); } } function highcharts_tooltip_factory(obsvalue, point_obsType, highchartsReturn=false, rounding, mirrored=false, numberFormat) { // Mirrored values have the negative sign removed if ( mirrored ) { obsvalue = Math.abs( obsvalue ); } if ( point_obsType == "windDir" ) { if (obsvalue >= 0 && obsvalue <= 11.25) { ordinal = "$ordinate_names[0]"; // N } else if (obsvalue >= 11.26 && obsvalue <= 33.75) { ordinal = "$ordinate_names[1]"; // NNE } else if (obsvalue >= 33.76 && obsvalue <= 56.25) { ordinal = "$ordinate_names[2]"; // NE } else if (obsvalue >= 56.26 && obsvalue <= 78.75) { ordinal = "$ordinate_names[3]"; // ENE } else if (obsvalue >= 78.76 && obsvalue <= 101.25) { ordinal = "$ordinate_names[4]"; // E } else if (obsvalue >= 101.26 && obsvalue <= 123.75) { ordinal = "$ordinate_names[5]"; // ESE } else if (obsvalue >= 123.76 && obsvalue <= 146.25) { ordinal = "$ordinate_names[6]"; // SE } else if (obsvalue >= 146.26 && obsvalue <= 168.75) { ordinal = "$ordinate_names[7]"; // SSE } else if (obsvalue >= 168.76 && obsvalue <= 191.25) { ordinal = "$ordinate_names[8]"; // S } else if (obsvalue >= 191.26 && obsvalue <= 213.75) { ordinal = "$ordinate_names[9]"; // SSW } else if (obsvalue >= 213.76 && obsvalue <= 236.25) { ordinal = "$ordinate_names[10]"; // SW } else if (obsvalue >= 236.26 && obsvalue <= 258.75) { ordinal = "$ordinate_names[11]"; // WSW } else if (obsvalue >= 258.76 && obsvalue <= 281.25) { ordinal = "$ordinate_names[12]"; // W } else if (obsvalue >= 281.26 && obsvalue <= 303.75) { ordinal = "$ordinate_names[13]"; // WNW } else if (obsvalue >= 303.76 && obsvalue <= 326.25) { ordinal = "$ordinate_names[14]"; // NW } else if (obsvalue >= 326.26 && obsvalue <= 348.75) { ordinal = "$ordinate_names[15]"; // NNW } else if (obsvalue >= 348.76 && obsvalue <= 360) { ordinal = "$ordinate_names[0]"; // N } // highchartsReturn returns the full wind direction string for highcharts tooltips. e.g "NNW (337)" if ( highchartsReturn ) { output = ordinal + " ("+ Math.round(obsvalue) + "\xBA)"; } else { output = ordinal; } } else { try { // Setup any graphs.conf overrides on formatting var { decimals, decimalPoint, thousandsSep } = numberFormat; // Try to apply the highcharts numberFormat for locale awareness. Use rounding from weewx.conf StringFormats. // -1 is set from Python to notate no rounding data available and decimals from graphs.conf is undefined. if ( rounding == "-1" && typeof decimals === "undefined" ) { output = Highcharts.numberFormat(obsvalue); } else { // If the amount of decimal is defined, use that instead since rounding is provided to the function. if ( typeof decimals !== "undefined" ) { rounding = decimals; } // If decimalPoint is undefined, use the auto detect from the skin since this comes from the skin. if ( typeof decimalPoint === "undefined" ) { decimalPoint = "$highcharts_decimal"; } // If thousandsSep is undefined, use the auto detect from the skin since this comes from the skin. if ( typeof thousandsSep === "undefined" ) { thousandsSep = "$highcharts_thousands"; } output = Highcharts.numberFormat(obsvalue, rounding, decimalPoint, thousandsSep); } } catch(err) { // Fall back to just returning the highcharts point number value, which is a best guess. output = Highcharts.numberFormat(obsvalue); } } return output; } // Handle wind arrow rotation with the ability to "rollover" past 0 // without spinning back around. e.g 350 to 3 would normally spin back around // https://stackoverflow.com/a/19872672/1177153 function rotateThis(newRotation) { if ( newRotation == "N/A") { return; } belchertown_debug("rotateThis: rotating to " + newRotation); var currentRotation; finalRotation = finalRotation || 0; // if finalRotation undefined or 0, make 0, else finalRotation currentRotation = finalRotation % 360; if ( currentRotation < 0 ) { currentRotation += 360; } if ( currentRotation < 180 && (newRotation > (currentRotation + 180)) ) { finalRotation -= 360; } if ( currentRotation >= 180 && (newRotation <= (currentRotation - 180)) ) { finalRotation += 360; } finalRotation += (newRotation - currentRotation); jQuery(".wind-arrow").css( "transform", "rotate(" + finalRotation + "deg)" ); jQuery(".arrow").css( "transform", "rotate(" + finalRotation + "deg)" ); } // Title case strings. https://stackoverflow.com/a/45253072/1177153 function titleCase(str) { return str.toLowerCase().split(' ').map(function(word) { return word.replace(word[0], word[0].toUpperCase()); }).join(' '); } function autoTheme(sunset_hour, sunset_min, sunrise_hour, sunrise_min) { // First check if ?theme= is in URL. If so, bail out and do not change anything. if ( getURLvar("theme") && getURLvar("theme") != "auto" ) { belchertown_debug("Auto theme: theme override detected in URL. Skipping auto theme"); return true; } belchertown_debug("Auto theme: checking to see if theme needs to be switched"); var d = new Date(); var nowHour = d.getHours(); var nowMinutes = d.getMinutes(); nowHour = nowHour; sunrise_hour = sunrise_hour; sunset_hour = sunset_hour; // Determine if it's day time. https://stackoverflow.com/a/14718577/1177153 if ( sunrise_hour <= nowHour && nowHour < sunset_hour ) { dayTime = true; } else { dayTime = false; } belchertown_debug("Auto theme: sunrise: " + sunrise_hour); belchertown_debug("Auto theme: now: " + nowHour); belchertown_debug("Auto theme: sunset: " + sunset_hour); belchertown_debug("Auto theme: are we in daylight hours: " + dayTime); belchertown_debug("Auto theme: sessionStorage.getItem('theme') = " + sessionStorage.getItem('theme')); if ( dayTime ) { // 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"); } } } 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"); } } } } function changeTheme(themeName, toggleOverride=false) { belchertown_debug("Theme: Changing to " + themeName); // If the configured theme is auto, but the user toggles light/dark, remove the auto option. if ( toggleOverride ) { belchertown_debug("Theme: toggle override clicked."); belchertown_debug("Theme: sessionStorage.getItem('theme') was previously: " + sessionStorage.getItem('theme') ); // This was applied only to auto theme config, but now it's applied to all themes so visitor has full control on light/dark mode //if ( sessionStorage.getItem('theme') == "auto" ) { } sessionStorage.setItem('theme', 'toggleOverride'); belchertown_debug("Theme: sessionStorage.getItem('theme') is now: " + sessionStorage.getItem('theme') ); } if ( themeName == "dark" ) { // Apply dark theme jQuery('body').addClass("dark"); jQuery('body').removeClass("light"); #if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1' jQuery("#themeSwitch").prop( "checked", true ); #end if #if $Extras.has_key('logo_image_dark') and $Extras.logo_image_dark != "" belchertown_debug("Theme: logo_image_dark is defined."); jQuery("#logo_image").attr("src","$Extras.logo_image_dark" ); #end if sessionStorage.setItem('currentTheme', 'dark'); } else if ( themeName == "light" ) { // Apply light theme jQuery('body').addClass("light"); jQuery('body').removeClass("dark"); #if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1' jQuery("#themeSwitch").prop( "checked", false ); #end if #if $Extras.has_key('logo_image') and $Extras.logo_image != "" belchertown_debug("Theme: logo_image is defined."); jQuery("#logo_image").attr("src", "$Extras.logo_image"); #end if sessionStorage.setItem('currentTheme', 'light'); } } function ajaxweewx() { // Relative URL jQuery.getJSON(get_relative_url() + "/json/weewx_data.json", update_weewx_data); }; // Update weewx data elements //var station_obs_array = ""; var unit_rounding_array = ""; var unit_label_array = ""; function update_weewx_data( data ) { belchertown_debug("Updating weewx data"); #if $Extras.has_key("theme") and $Extras.theme == 'auto' // Auto theme if enabled autoTheme(data["almanac"]["sunset_hour"], data["almanac"]["sunset_minute"], data["almanac"]["sunrise_hour"], data["almanac"]["sunrise_minute"] ); #end if //station_obs_array = data["station_observations"]; unit_rounding_array = data["unit_rounding"]; unit_label_array = data["unit_label"]; // Daily High Low high = data["day"]["outTemp"]["max"]; low = data["day"]["outTemp"]["min"]; jQuery(".high").html( high ); jQuery(".low").html( low ); // Barometer trending by finding a negative number count = ( data["current"]["barometer_trend"].match(/-/g) || [] ).length if ( count >= 1 ) { jQuery(".pressure-trend").html( '' ); } else { jQuery(".pressure-trend").html( '' ); } // Daily max gust span jQuery(".dailymaxgust").html( parseFloat( data["day"]["wind"]["max"] ).toFixed(1) ); // Daily Snapshot Stats Section jQuery(".snapshot-records-today-header").html( moment.unix( data["current"]["epoch"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_snapshot_records_today_header' ) ); jQuery(".snapshot-records-month-header").html( moment.unix( data["current"]["epoch"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_snapshot_records_month_header' ) ); jQuery(".dailystatshigh").html( data["day"]["outTemp"]["max"] ); jQuery(".dailystatslow").html( data["day"]["outTemp"]["min"] ); jQuery(".dailystatswindavg").html( data["day"]["wind"]["average"] ); jQuery(".dailystatswindmax").html( data["day"]["wind"]["max"] ); jQuery(".dailystatsrain").html( data["day"]["rain"]["sum"] ); jQuery(".dailystatsrainrate").html( data["day"]["rain"]["max"] ); // Month Snapshot Stats Section jQuery(".monthstatshigh").html( data["month"]["outTemp"]["max"] ); jQuery(".monthstatslow").html( data["month"]["outTemp"]["min"] ); jQuery(".monthstatswindavg").html( data["month"]["wind"]["average"] ); jQuery(".monthstatswindmax").html( data["month"]["wind"]["max"] ); jQuery(".monthstatsrain").html( data["month"]["rain"]["sum"] ); jQuery(".monthstatsrainrate").html( data["month"]["rain"]["max"] ); // Sunrise and Sunset jQuery(".sunrise-value").html( moment.unix( parseFloat(data["almanac"]["sunrise_epoch"]).toFixed(0) ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_sunrise" ) ); jQuery(".sunset-value").html( moment.unix( parseFloat(data["almanac"]["sunset_epoch"]).toFixed(0) ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_sunset" ) ); // Moon icon, phase and illumination percent switch ( data["almanac"]["moon"]["moon_index"] ) { case "0": jQuery(".moon-icon").html( "
" ); break; case "1": jQuery(".moon-icon").html( "
" ); break; case "2": jQuery(".moon-icon").html( "
" ); break; case "3": jQuery(".moon-icon").html( "
" ); break; case "4": jQuery(".moon-icon").html( "
" ); break; case "5": jQuery(".moon-icon").html( "
" ); break; case "6": jQuery(".moon-icon").html( "
" ); break; case "7": jQuery(".moon-icon").html( "
" ); break; } jQuery(".moon-phase").html( titleCase( data["almanac"]["moon"]["moon_phase"] ) ); // Javascript function above jQuery(".moon-visible").html( "" + data["almanac"]["moon"]["moon_fullness"] + "% $obs.label.moon_visible" ); #if $almanac.hasExtras // Close current modal if open jQuery('#almanac').modal('hide'); jQuery(".almanac-extras-modal-body").html( data["almanac"]["almanac_extras_modal_html"] ); almanac_updated = "$obs.label.header_last_updated " + moment.unix( data["current"]["datetime_raw"] ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); jQuery(".almanac_last_updated").html( almanac_updated ); #end if } #if $Extras.has_key("forecast_enabled") and $Extras.forecast_enabled == '1' function ajaxforecast() { jQuery.getJSON( get_relative_url() + "/json/forecast.json", update_forecast_data); }; function aeris_coded_weather( data, full_observation = false ) { // https://www.aerisweather.com/support/docs/api/reference/weather-codes/ var output = ""; var coverage_code = data.split(":")[0] var intensity_code = data.split(":")[1] var weather_code = data.split(":")[2] var cloud_dict = { "CL": "$obs.label.forecast_cloud_code_CL", "FW": "$obs.label.forecast_cloud_code_FW", "SC": "$obs.label.forecast_cloud_code_SC", "BK": "$obs.label.forecast_cloud_code_BK", "OV": "$obs.label.forecast_cloud_code_OV" } var coverage_dict = { "AR": "$obs.label.forecast_coverage_code_AR", "BR": "$obs.label.forecast_coverage_code_BR", "C": "$obs.label.forecast_coverage_code_C", "D": "$obs.label.forecast_coverage_code_D", "FQ": "$obs.label.forecast_coverage_code_FQ", "IN": "$obs.label.forecast_coverage_code_IN", "IS": "$obs.label.forecast_coverage_code_IS", "L": "$obs.label.forecast_coverage_code_L", "NM": "$obs.label.forecast_coverage_code_NM", "O": "$obs.label.forecast_coverage_code_O", "PA": "$obs.label.forecast_coverage_code_PA", "PD": "$obs.label.forecast_coverage_code_PD", "S": "$obs.label.forecast_coverage_code_S", "SC": "$obs.label.forecast_coverage_code_SC", "VC": "$obs.label.forecast_coverage_code_VC", "WD": "$obs.label.forecast_coverage_code_WD" } var intensity_dict = { "VL": "$obs.label.forecast_intensity_code_VL", "L": "$obs.label.forecast_intensity_code_L", "H": "$obs.label.forecast_intensity_code_H", "VH": "$obs.label.forecast_intensity_code_VH" } var weather_dict = { "A": "$obs.label.forecast_weather_code_A", "BD": "$obs.label.forecast_weather_code_BD", "BN": "$obs.label.forecast_weather_code_BN", "BR": "$obs.label.forecast_weather_code_BR", "BS": "$obs.label.forecast_weather_code_BS", "BY": "$obs.label.forecast_weather_code_BY", "F": "$obs.label.forecast_weather_code_F", "FR": "$obs.label.forecast_weather_code_FR", "H": "$obs.label.forecast_weather_code_H", "IC": "$obs.label.forecast_weather_code_IC", "IF": "$obs.label.forecast_weather_code_IF", "IP": "$obs.label.forecast_weather_code_IP", "K": "$obs.label.forecast_weather_code_K", "L": "$obs.label.forecast_weather_code_L", "R": "$obs.label.forecast_weather_code_R", "RW": "$obs.label.forecast_weather_code_RW", "RS": "$obs.label.forecast_weather_code_RS", "SI": "$obs.label.forecast_weather_code_SI", "WM": "$obs.label.forecast_weather_code_WM", "S": "$obs.label.forecast_weather_code_S", "SW": "$obs.label.forecast_weather_code_SW", "T": "$obs.label.forecast_weather_code_T", "UP": "$obs.label.forecast_weather_code_UP", "VA": "$obs.label.forecast_weather_code_VA", "WP": "$obs.label.forecast_weather_code_WP", "ZF": "$obs.label.forecast_weather_code_ZF", "ZL": "$obs.label.forecast_weather_code_ZL", "ZR": "$obs.label.forecast_weather_code_ZR", "ZY": "$obs.label.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 ( cloud_dict.hasOwnProperty( weather_code ) ) { return cloud_dict[weather_code]; } else { // Add the coverage if it's present, and full observation forecast is requested if ( ( coverage_code ) && ( full_observation ) ) { 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; } function aeris_coded_alerts( data, full_observation = false ) { // https://www.aerisweather.com/support/docs/aeris-maps/reference/alert-types/ var alert_dict = { "TOE": "$obs.label.forecast_alert_code_TOE", "ADR": "$obs.label.forecast_alert_code_ADR", "AQA": "$obs.label.forecast_alert_code_AQA", "AQ.S": "$obs.label.forecast_alert_code_AQ_S", "AS.Y": "$obs.label.forecast_alert_code_AS_Y", "AR.W": "$obs.label.forecast_alert_code_AR_W", "AF.Y": "$obs.label.forecast_alert_code_AF_Y", "MH.Y": "$obs.label.forecast_alert_code_MH_Y", "AF.W": "$obs.label.forecast_alert_code_AF_W", "AVW": "$obs.label.forecast_alert_code_AVW", "AVA": "$obs.label.forecast_alert_code_AVA", "BH.S": "$obs.label.forecast_alert_code_BH_S", "BZ.W": "$obs.label.forecast_alert_code_BZ_W", "DU.Y": "$obs.label.forecast_alert_code_DU_Y", "BS.Y": "$obs.label.forecast_alert_code_BS_Y", "BW.Y": "$obs.label.forecast_alert_code_BW_Y", "CAE": "$obs.label.forecast_alert_code_CAE", "CDW": "$obs.label.forecast_alert_code_CDW", "CEM": "$obs.label.forecast_alert_code_CEM", "CF.Y": "$obs.label.forecast_alert_code_CF_Y", "CF.S": "$obs.label.forecast_alert_code_CF_S", "CF.W": "$obs.label.forecast_alert_code_CF_W", "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", "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", "EQW": "$obs.label.forecast_alert_code_EQW", "EVI": "$obs.label.forecast_alert_code_EVI", "EH.W": "$obs.label.forecast_alert_code_EH_W", "EH.A": "$obs.label.forecast_alert_code_EH_A", "EC.W": "$obs.label.forecast_alert_code_EC_W", "EC.A": "$obs.label.forecast_alert_code_EC_A", "RFD": "$obs.label.forecast_alert_code_RFD", "EW.W": "$obs.label.forecast_alert_code_EW_W", "FRW": "$obs.label.forecast_alert_code_FRW", "FW.A": "$obs.label.forecast_alert_code_FW_A", "FF.S": "$obs.label.forecast_alert_code_FF_S", "FF.W": "$obs.label.forecast_alert_code_FF_W", "FF.A": "$obs.label.forecast_alert_code_FF_A", "FE.W": "$obs.label.forecast_alert_code_FE_W", "FL.Y": "$obs.label.forecast_alert_code_FL_Y", "FL.S": "$obs.label.forecast_alert_code_FL_S", "FL.W": "$obs.label.forecast_alert_code_FL_W", "FA.W": "$obs.label.forecast_alert_code_FA_W", "FL.A": "$obs.label.forecast_alert_code_FL_A", "FA.A": "$obs.label.forecast_alert_code_FA_A", "FZ.W": "$obs.label.forecast_alert_code_FZ_W", "FZ.A": "$obs.label.forecast_alert_code_FZ_A", "ZL.Y": "$obs.label.forecast_alert_code_ZL_Y", "ZF.Y": "$obs.label.forecast_alert_code_ZF_Y", "ZR.W": "$obs.label.forecast_alert_code_ZR_W", "UP.Y": "$obs.label.forecast_alert_code_UP_Y", "FR.Y": "$obs.label.forecast_alert_code_FR_Y", "GL.W": "$obs.label.forecast_alert_code_GL_W", "GL.A": "$obs.label.forecast_alert_code_GL_A", "HZ.W": "$obs.label.forecast_alert_code_HZ_W", "HZ.A": "$obs.label.forecast_alert_code_HZ_A", "HMW": "$obs.label.forecast_alert_code_HMW", "SE.W": "$obs.label.forecast_alert_code_SE_W", "SE.A": "$obs.label.forecast_alert_code_SE_A", "HWO": "$obs.label.forecast_alert_code_HWO", "HT.Y": "$obs.label.forecast_alert_code_HT_Y", "HT.W": "$obs.label.forecast_alert_code_HT_W", "UP.W": "$obs.label.forecast_alert_code_UP_W", "UP.A": "$obs.label.forecast_alert_code_UP_A", "SU.Y": "$obs.label.forecast_alert_code_SU_Y", "SU.W": "$obs.label.forecast_alert_code_SU_W", "HW.W": "$obs.label.forecast_alert_code_HW_W", "HW.A": "$obs.label.forecast_alert_code_HW_A", "HF.W": "$obs.label.forecast_alert_code_HF_W", "HF.A": "$obs.label.forecast_alert_code_HF_A", "HU.S": "$obs.label.forecast_alert_code_HU_S", "HU.W": "$obs.label.forecast_alert_code_HU_W", "HU.A": "$obs.label.forecast_alert_code_HU_A", "FA.Y": "$obs.label.forecast_alert_code_FA_Y", "IS.W": "$obs.label.forecast_alert_code_IS_W", "LE.W": "$obs.label.forecast_alert_code_LE_W", "LW.Y": "$obs.label.forecast_alert_code_LW_Y", "LS.Y": "$obs.label.forecast_alert_code_LS_Y", "LS.S": "$obs.label.forecast_alert_code_LS_S", "LS.W": "$obs.label.forecast_alert_code_LS_W", "LS.A": "$obs.label.forecast_alert_code_LS_A", "LEW": "$obs.label.forecast_alert_code_LEW", "LAE": "$obs.label.forecast_alert_code_LAE", "LO.Y": "$obs.label.forecast_alert_code_LO_Y", "MA.S": "$obs.label.forecast_alert_code_MA_S", "NUW": "$obs.label.forecast_alert_code_NUW", "RHW": "$obs.label.forecast_alert_code_RHW", "RA.W": "$obs.label.forecast_alert_code_RA_W", "FW.W": "$obs.label.forecast_alert_code_FW_W", "RFW": "$obs.label.forecast_alert_code_RFW", "RP.S": "$obs.label.forecast_alert_code_RP_S", "SV.W": "$obs.label.forecast_alert_code_SV_W", "SV.A": "$obs.label.forecast_alert_code_SV_A", "SV.S": "$obs.label.forecast_alert_code_SV_S", "TO.S": "$obs.label.forecast_alert_code_TO_S", "SPW": "$obs.label.forecast_alert_code_SPW", "NOW": "$obs.label.forecast_alert_code_NOW", "SC.Y": "$obs.label.forecast_alert_code_SC_Y", "SW.Y": "$obs.label.forecast_alert_code_SW_Y", "RB.Y": "$obs.label.forecast_alert_code_RB_Y", "SI.Y": "$obs.label.forecast_alert_code_SI_Y", "SO.W": "$obs.label.forecast_alert_code_SO_W", "SQ.W": "$obs.label.forecast_alert_code_SQ_W", "SQ.A": "$obs.label.forecast_alert_code_SQ_A", "SB.Y": "$obs.label.forecast_alert_code_SB_Y", "SN.W": "$obs.label.forecast_alert_code_SN_W", "MA.W": "$obs.label.forecast_alert_code_MA_W", "SPS": "$obs.label.forecast_alert_code_SPS", "SG.W": "$obs.label.forecast_alert_code_SG_W", "SS.W": "$obs.label.forecast_alert_code_SS_W", "SS.A": "$obs.label.forecast_alert_code_SS_A", "SR.W": "$obs.label.forecast_alert_code_SR_W", "SR.A": "$obs.label.forecast_alert_code_SR_A", "TO.W": "$obs.label.forecast_alert_code_TO_W", "TO.A": "$obs.label.forecast_alert_code_TO_A", "TC.S": "$obs.label.forecast_alert_code_TC_S", "TR.S": "$obs.label.forecast_alert_code_TR_S", "TR.W": "$obs.label.forecast_alert_code_TR_W", "TR.A": "$obs.label.forecast_alert_code_TR_A", "TS.Y": "$obs.label.forecast_alert_code_TS_Y", "TS.W": "$obs.label.forecast_alert_code_TS_W", "TS.A": "$obs.label.forecast_alert_code_TS_A", "TY.S": "$obs.label.forecast_alert_code_TY_S", "TY.W": "$obs.label.forecast_alert_code_TY_W", "TY.A": "$obs.label.forecast_alert_code_TY_A", "VOW": "$obs.label.forecast_alert_code_VOW", "WX.Y": "$obs.label.forecast_alert_code_WX_Y", "WX.W": "$obs.label.forecast_alert_code_WX_W", "WI.Y": "$obs.label.forecast_alert_code_WI_Y", "WC.Y": "$obs.label.forecast_alert_code_WC_Y", "WC.W": "$obs.label.forecast_alert_code_WC_W", "WC.A": "$obs.label.forecast_alert_code_WC_A", "WI.W": "$obs.label.forecast_alert_code_WI_W", "WS.W": "$obs.label.forecast_alert_code_WS_W", "WS.A": "$obs.label.forecast_alert_code_WS_A", "LE.A": "$obs.label.forecast_alert_code_LE_A", "BZ.A": "$obs.label.forecast_alert_code_BZ_A", "WW.Y": "$obs.label.forecast_alert_code_WW_Y", "LE.Y": "$obs.label.forecast_alert_code_LE_Y", "ZR.Y": "$obs.label.forecast_alert_code_ZR_Y", "AW.WI.MN": "$obs.label.forecast_alert_code_AW_WI_MN", "AW.WI.MD": "$obs.label.forecast_alert_code_AW_WI_MD", "AW.WI.SV": "$obs.label.forecast_alert_code_AW_WI_SV", "AW.WI.EX": "$obs.label.forecast_alert_code_AW_WI_EX", "AW.SI.MN": "$obs.label.forecast_alert_code_AW_SI_MN", "AW.SI.MD": "$obs.label.forecast_alert_code_AW_SI_MD", "AW.SI.SV": "$obs.label.forecast_alert_code_AW_SI_SV", "AW.SI.EX": "$obs.label.forecast_alert_code_AW_SI_EX", "AW.TS.MN": "$obs.label.forecast_alert_code_AW_TS_MN", "AW.TS.MD": "$obs.label.forecast_alert_code_AW_TS_MD", "AW.TS.SV": "$obs.label.forecast_alert_code_AW_TS_SV", "AW.TS.EX": "$obs.label.forecast_alert_code_AW_TS_EX", "AW.LI.MN": "$obs.label.forecast_alert_code_AW_LI_MN", "AW.LI.MD": "$obs.label.forecast_alert_code_AW_LI_MD", "AW.LI.SV": "$obs.label.forecast_alert_code_AW_LI_SV", "AW.LI.EX": "$obs.label.forecast_alert_code_AW_LI_EX", "AW.FG.MN": "$obs.label.forecast_alert_code_AW_FG_MN", "AW.FG.MD": "$obs.label.forecast_alert_code_AW_FG_MD", "AW.FG.SV": "$obs.label.forecast_alert_code_AW_FG_SV", "AW.FG.EX": "$obs.label.forecast_alert_code_AW_FG_EX", "AW.HT.MN": "$obs.label.forecast_alert_code_AW_HT_MN", "AW.HT.MD": "$obs.label.forecast_alert_code_AW_HT_MD", "AW.HT.SV": "$obs.label.forecast_alert_code_AW_HT_SV", "AW.HT.EX": "$obs.label.forecast_alert_code_AW_HT_EX", "AW.LT.MN": "$obs.label.forecast_alert_code_AW_LT_MN", "AW.LT.MD": "$obs.label.forecast_alert_code_AW_LT_MD", "AW.LT.SV": "$obs.label.forecast_alert_code_AW_LT_SV", "AW.LT.EX": "$obs.label.forecast_alert_code_AW_LT_EX", "AW.CE.MN": "$obs.label.forecast_alert_code_AW_CE_MN", "AW.CE.MD": "$obs.label.forecast_alert_code_AW_CE_MD", "AW.CE.SV": "$obs.label.forecast_alert_code_AW_CE_SV", "AW.CE.EX": "$obs.label.forecast_alert_code_AW_CE_EX", "AW.FR.MN": "$obs.label.forecast_alert_code_AW_FR_MN", "AW.FR.MD": "$obs.label.forecast_alert_code_AW_FR_MD", "AW.FR.SV": "$obs.label.forecast_alert_code_AW_FR_SV", "AW.FR.EX": "$obs.label.forecast_alert_code_AW_FR_EX", "AW.AV.MN": "$obs.label.forecast_alert_code_AW_AV_MN", "AW.AV.MD": "$obs.label.forecast_alert_code_AW_AV_MD", "AW.AV.SV": "$obs.label.forecast_alert_code_AW_AV_SV", "AW.AV.EX": "$obs.label.forecast_alert_code_AW_AV_EX", "AW.RA.MN": "$obs.label.forecast_alert_code_AW_RA_MN", "AW.RA.MD": "$obs.label.forecast_alert_code_AW_RA_MD", "AW.RA.SV": "$obs.label.forecast_alert_code_AW_RA_SV", "AW.RA.EX": "$obs.label.forecast_alert_code_AW_RA_EX", "AW.FL.MN": "$obs.label.forecast_alert_code_AW_FL_MN", "AW.FL.MD": "$obs.label.forecast_alert_code_AW_FL_MD", "AW.FL.SV": "$obs.label.forecast_alert_code_AW_FL_SV", "AW.FL.EX": "$obs.label.forecast_alert_code_AW_FL_EX", "AW.RF.MN": "$obs.label.forecast_alert_code_AW_RF_MN", "AW.RF.MD": "$obs.label.forecast_alert_code_AW_RF_MD", "AW.RF.SV": "$obs.label.forecast_alert_code_AW_RF_SV", "AW.RF.EX": "$obs.label.forecast_alert_code_AW_RF_EX", "AW.UK.MN": "$obs.label.forecast_alert_code_AW_UK_MN", "AW.UK.MD": "$obs.label.forecast_alert_code_AW_UK_MD", "AW.UK.SV": "$obs.label.forecast_alert_code_AW_UK_SV", "AW.UK.EX": "$obs.label.forecast_alert_code_AW_UK_EX" } return alert_dict[data]; } function aeris_icon( data ) { // https://www.aerisweather.com/support/docs/api/reference/icon-list/ icon_name = data.split(".")[0]; // Remove .png var 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", "na": "unknown", "pcloudy": "partly-cloudy-day", "pcloudyn": "partly-cloudy-night", "pcloudyr": "rain", "pcloudyrn": "rain", "pcloudyrw": "rain", "pcloudyrwn": "rain", "pcloudys": "snow", "pcloudysn": "snow", "pcloudysf": "snow", "pcloudysfn": "snow", "pcloudysfw": "snow", "pcloudysfwn": "snow", "pcloudysw": "partly-cloudy-day", "pcloudyswn": "partly-cloudy-night", "pcloudyt": "thunderstorm", "pcloudytn": "thunderstorm", "pcloudytw": "thunderstorm", "pcloudytwn": "thunderstorm", "pcloudyw": "partly-cloudy-day", "pcloudywn": "partly-cloudy-night", "rain": "rain", "rainn": "rain", "rainandsnow": "rain", "rainandsnown": "rain", "raintosnow": "rain", "raintosnown": "rain", "rainw": "rain", "rainw": "rain", "showers": "rain", "showersn": "rain", "showersw": "rain", "showersw": "rain", "sleet": "sleet", "sleetn": "sleet", "sleetsnow": "sleet", "sleetsnown": "sleet", "smoke": "fog", "smoken": "fog", "snow": "snow", "snown": "snow", "snoww": "snow", "snowwn": "snow", "snowshowers": "snow", "snowshowersn": "snow", "snowshowersw": "snow", "snowshowerswn": "snow", "snowtorain": "snow", "snowtorainn": "snow", "sunny": "clear-day", "sunnyn": "clear-night", "sunnyw": "mostly-clear-day", "sunnywn": "mostly-clear-night", "tstorm": "thunderstorm", "tstormn": "thunderstorm", "tstorms": "thunderstorm", "tstormsn": "thunderstorm", "tstormsw": "thunderstorm", "tstormswn": "thunderstorm", "wind": "wind", "wind": "wind", "wintrymix": "sleet", "wintrymixn": "sleet" } return icon_dict[icon_name]; } function show_forcast_alert( data, forecast_provider ) { belchertown_debug("Forecast: Updating alert data for " + forecast_provider); var i, forecast_alert_modal, forecast_alerts; forecast_alert_modal = ""; forecast_alerts = []; // Empty anything that's been appended to the modal from the previous run jQuery(".wx-stn-alert-text").empty(); if ( forecast_provider == "darksky" ) { if ( data['alerts'] ) { for ( i = 0; i < data['alerts'].length; i++ ) { forecast_alert_title = data['alerts'][i]['title']; forecast_alert_body = data['alerts'][i]['description'].replace(/\n/g, '
'); forecast_alert_link = data['alerts'][i]['title']; forecast_alert_expires = moment.unix( data['alerts'][i]['expires'] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_alert_expires' ); forecast_alerts.push( { "title": forecast_alert_title, "body": forecast_alert_body, "link": forecast_alert_link, "expires": forecast_alert_expires }); } } } else if ( forecast_provider == "aeris" ) { if ( data['alerts'][0]['response'][0] ) { for ( i = 0; i < data['alerts'][0]['response'].length; i++ ) { //forecast_alert_title = data['alerts'][0]['response'][i]['details']['name']; forecast_alert_title = aeris_coded_alerts( data['alerts'][0]['response'][i]['details']['type'] ); forecast_alert_body = data['alerts'][0]['response'][i]['details']['body'].replace(/\n/g, '
'); //forecast_alert_link = data['alerts'][0]['response'][i]['details']['name']; forecast_alert_link = data['alerts'][0]['response'][i]['details']['type']; forecast_alert_expires = moment.unix( data['alerts'][0]['response'][i]['timestamps']['expires'] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_alert_expires' ); forecast_alerts.push( { "title": forecast_alert_title, "body": forecast_alert_body, "link": forecast_alert_link, "expires": forecast_alert_expires }); } } } if ( forecast_alerts.length > 0 ) { belchertown_debug("Forecast: There are " + forecast_alerts.length + " alert(s)."); for ( i = 0; i < forecast_alerts.length; i++ ) { alert_link = " " +forecast_alerts[i]["title"] + " $obs.label.alert_in_effect " + forecast_alerts[i]["expires"] + "
"; jQuery(".wx-stn-alert-text").append( alert_link ); forecast_alert_modal += ""; forecast_alert_modal += ""; jQuery(".wx-stn-alert-text").append( forecast_alert_modal ); } jQuery(".wx-stn-alert").show(); } else { belchertown_debug("Forecast: There are no forecast alerts"); jQuery(".wx-stn-alert").hide(); } } function update_forecast_data( data ) { forecast_provider = "$Extras.forecast_provider"; belchertown_debug("Forecast: Provider is " + forecast_provider); belchertown_debug("Forecast: Updating data"); forecast_row = []; if ( forecast_provider == "N/A" ) { jQuery(".forecastrow").hide(); belchertown_debug("Forecast: No provider, hiding forecastrow"); return; } else if ( forecast_provider == "darksky" ) { var forecast_subtitle = moment.unix( data["currently"]["time"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_last_updated' ); // WX icon in temperature box if ( data['currently']['icon'] == "partly-cloudy-night" ) { // partly-cloudy-night could also be clear-day var wxicon = get_relative_url() + "/images/partly-cloudy-night.png"; } else { var wxicon = get_relative_url() + "/images/" + data['currently']['icon'] + ".png"; } belchertown_debug("Forecast: icon is " + wxicon); // Current observation text jQuery(".current-obs-text").html( data["currently"]["summary"] ); // Visibility text in station observation table try { visibility_output = parseFloat(parseFloat(data["currently"]["visibility"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["visibility"], maximumFractionDigits: unit_rounding_array["visibility"]}) + " " + unit_label_array["visibility"]; jQuery(".station-observations .visibility").html( visibility_output ); } catch(err) { // Visibility not in the station observation table or any of the unit arrays, so silently exit } // Force 7 day forecast //for (i = 0; i < data["daily"]["data"].length; i++) { for (i = 0; i < 7; i++) { var weekday = moment.unix( data["daily"]["data"][i]["time"] + 7200 ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_forecast_date" ); if ( data['daily']['data'][i]['icon'] == "partly-cloudy-night" ) { var image_url = get_relative_url() + "/images/clear-day.png"; } else { var image_url = get_relative_url() + "/images/"+data['daily']['data'][i]['icon']+".png"; } var condition_text = ""; switch ( data["daily"]["data"][i]["icon"] ) { case "clear-day": condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear break; case "clear-night": condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear break; case "rain": condition_text = "$obs.label.forecast_weather_code_R"; // Rain break; case "snow": condition_text = "$obs.label.forecast_weather_code_S"; // Snow break; case "sleet": condition_text = "$obs.label.forecast_weather_code_IP"; // Sleet break; case "wind": condition_text = "$obs.label.forecast_weather_code_W"; // Windy break; case "fog": condition_text = "$obs.label.forecast_weather_code_F"; // Fog break; case "cloudy": condition_text = "$obs.label.forecast_cloud_code_OV"; // Overcast break; case "partly-cloudy-day": condition_text = "$obs.label.forecast_cloud_code_SC"; // Partly Cloudy break; case "partly-cloudy-night": condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear - https://darksky.net/dev/docs/faq - So you can just treat partly-cloudy-night as an alias for clear-day. break; case "hail": condition_text = "$obs.label.forecast_weather_code_A"; // Hail break; case "thunderstorm": condition_text = "$obs.label.forecast_weather_code_T"; // Thunderstorm break; case "tornado": condition_text = "$obs.label.forecast_weather_code_TO"; // Tornado break; } if ( data["daily"]["data"][i].hasOwnProperty("precipType") && data["daily"]["data"][i]["precipType"] == "snow" ) { var snow_depth = data["daily"]["data"][i]["precipAccumulation"]; var snow_unit = "in"; } else if ( data["daily"]["data"][i].hasOwnProperty("precipType") && data["daily"]["data"][i]["precipType"] == "rain" ) { var precip = data["daily"]["data"][i]["precipProbability"] * 100; } else { var precip = 0; var snow_depth = 0; var snow_unit = ""; } var darksky_link_units = data["flags"]["units"].toLowerCase().concat("12"); // Currently the DarkSky forecast link uses a "{unit}12" string in the URL. #if $Extras.has_key("darksky_lang") var forecast_link = '$obs.label.daily_forecast'; #else var forecast_link = '$obs.label.daily_forecast'; #end if forecast_row.push( { "weekday": weekday, "image_url": image_url, "condition_text": condition_text, "minTemp": data["daily"]["data"][i]["temperatureLow"], "maxTemp": data["daily"]["data"][i]["temperatureHigh"], "windSpeed": data["daily"]["data"][i]["windSpeed"], "windGust": data["daily"]["data"][i]["windGust"], "snow_depth": snow_depth, "snow_unit": snow_unit, "precip": precip, "forecast_link": forecast_link, } ); } } if ( forecast_provider == "aeris" ) { var forecast_subtitle = moment.unix( data["timestamp"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_last_updated' ); try { belchertown_debug("Forecast: icon from Aeris data is " + data["current"][0]["response"]["ob"]["icon"] ); var wxicon = get_relative_url() + "/images/" + aeris_icon( data["current"][0]["response"]["ob"]["icon"] ) + ".png"; belchertown_debug("Forecast: Belchertown icon is " + wxicon); // Current observation text jQuery(".current-obs-text").html( aeris_coded_weather( data["current"][0]["response"]["ob"]["weatherPrimaryCoded"], true ) ); // Visibility text in station observation table if ( ( "$Extras.forecast_units" == "si" ) || ( "$Extras.forecast_units" == "ca" ) ) { // si and ca = kilometer visibility = data["current"][0]["response"]["ob"]["visibilityKM"]; } else { // us and uk2 and default = miles visibility = data["current"][0]["response"]["ob"]["visibilityMI"]; } try { visibility_output = parseFloat(parseFloat( visibility )).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["visibility"], maximumFractionDigits: unit_rounding_array["visibility"]}) + " " + unit_label_array["visibility"]; jQuery(".station-observations .visibility").html( visibility_output ); } catch(err) { // Visibility not in the station observation table or any of the unit arrays, so silently exit } } catch(err) { // Probably a non-metar current observation, which does not have visibility, icon, conditions, etc. So silently exit. } for (i = 0; i < data["forecast"][0]["response"][0]["periods"].length; i++) { var image_url = get_relative_url() + "/images/" + aeris_icon( data["forecast"][0]["response"][0]["periods"][i]["icon"] ) + ".png"; var condition_text = aeris_coded_weather( data["forecast"][0]["response"][0]["periods"][i]["weatherPrimaryCoded"], false ); var weekday = moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_forecast_date" ); // Determine temperature units if ( ( "$Extras.forecast_units" == "ca" ) || ( "$Extras.forecast_units" == "uk2" ) || ( "$Extras.forecast_units" == "si" ) ) { minTemp = data["forecast"][0]["response"][0]["periods"][i]["minTempC"]; maxTemp = data["forecast"][0]["response"][0]["periods"][i]["maxTempC"]; } else { // Default minTemp = data["forecast"][0]["response"][0]["periods"][i]["minTempF"]; maxTemp = data["forecast"][0]["response"][0]["periods"][i]["maxTempF"]; } // Determine wind units if ( "$Extras.forecast_units" == "ca" ) { // ca = kph windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedKPH"]; windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustKPH"]; } else if ( "$Extras.forecast_units" == "si" ) { // si = meters per second. MPS is KPH / 3.6 windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedKPH"] / 3.6; windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustKPH"] / 3.6; } else { // us and uk2 and default = mph windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedMPH"]; windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustMPH"]; } if ( ( data["forecast"][0]["response"][0]["periods"][i]["snowCM"] > 0 ) || ( data["forecast"][0]["response"][0]["periods"][i]["snowIN"] > 0 ) ) { // Determine snow unit if ( ( "$Extras.forecast_units" == "si" ) || ( "$Extras.forecast_units" == "ca" ) || ( "$Extras.forecast_units" == "uk2" ) ) { snow_depth = data["forecast"][0]["response"][0]["periods"][i]["snowCM"]; snow_unit = "cm"; } else { snow_depth = data["forecast"][0]["response"][0]["periods"][i]["snowIN"]; snow_unit = "in"; } } else if ( data["forecast"][0]["response"][0]["periods"][i]["pop"] > 0 ) { // Rain percent of precip precip = data["forecast"][0]["response"][0]["periods"][i]["pop"]; } else { precip = 0; snow_depth = 0; snow_unit = ""; } var forecast_link_setup = "$Extras.forecast_daily_forecast_link".replace("YYYY", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "YYYY" )).replace("MM", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "MM" )).replace("DD", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "DD" )); var forecast_link = '$obs.label.daily_forecast'; forecast_row.push( { "weekday": weekday, "image_url": image_url, "condition_text": condition_text, "minTemp": minTemp, "maxTemp": maxTemp, "windSpeed": windSpeed, "windGust": windGust, "snow_depth": snow_depth, "snow_unit": snow_unit, "precip": precip, "forecast_link": forecast_link } ); } } // WX icon in temperature box jQuery("#wxicon").attr( "src", wxicon ); belchertown_debug("Forecast: Changing icon to " + wxicon); // Build daily forecast row var output_html = ""; for (i = 0; i < forecast_row.length; i++) { if ( i == 0 ) { output_html += '
'; } else { output_html += '
'; } // Add 7200 (2 hours) to the epoch to get an hour well into the day to avoid any DST issues. This way it'll either be 1am or 2am. Without it, we get 12am or 11pm (the previous day). output_html += ''+forecast_row[i]["weekday"]+''; output_html += '
'; output_html += '
'; output_html += ''; output_html += ''+forecast_row[i]["condition_text"]+''; output_html += '
'; output_html += ''+ parseFloat( forecast_row[i]["maxTemp"] ).toFixed(0) +'° | '+ parseFloat( forecast_row[i]["minTemp"] ).toFixed(0) +'°'; output_html += '
'; output_html += '
'; if ( forecast_row[i]["snow_depth"] > 0 ) { 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 ) { output_html += ' '+ parseFloat( forecast_row[i]["precip"] ).toFixed(0) +'%'; } else { output_html += ' 0%'; } output_html += '
'; output_html += '
'; output_html += ' '+ parseFloat( forecast_row[i]["windSpeed"] ).toFixed(0) +' | '+ parseFloat( forecast_row[i]["windGust"] ).toFixed(0) +'$unit.label.windSpeed'; output_html += '
'; #if $Extras.has_key("forecast_show_daily_forecast_link") and $Extras.forecast_show_daily_forecast_link == '1' output_html += forecast_row[i]["forecast_link"]; #end if output_html += '
'; } // Show the forecast row jQuery(".forecast-subtitle").html( "$obs.label.forecast_last_updated " + forecast_subtitle ); jQuery(".forecasts").html( output_html ); #if $Extras.has_key("forecast_alert_enabled") and $Extras.forecast_alert_enabled == '1' // Show weather alert show_forcast_alert( data, forecast_provider ); #end if } #end if #if $Extras.has_key("mqtt_websockets_enabled") and $Extras.mqtt_websockets_enabled == '1' //============================================// // Live website using MQTT Websockets enabled // //============================================// var mqttConnected = false; function ajaximages(section=false, reload_timer_interval_seconds=false) { // This function only runs if the elements have an img src. // Update images within the specific section if ( section ) { if ( section == "radar" ) { belchertown_debug("Updating radar image"); // Reload images if ( document.querySelectorAll(".radar-map img").length > 0 ) { var radar_img = document.querySelectorAll(".radar-map img")[0].src; var new_radar_img = radar_img + "&t=" + Math.floor(Math.random() * 999999999); document.querySelectorAll(".radar-map img")[0].src = new_radar_img; //var radar_html = jQuery('.radar-map').children('img').attr('src').split('?')[0] // Get the img src and remove everything after "?" so we don't stack ?'s onto the image during updates //jQuery('.radar-map').children('img').attr('src', radar_html + "?" + Math.floor(Math.random() * 999999999)); } // Reload iframe - https://stackoverflow.com/a/4249946/1177153 if ( document.querySelectorAll(".radar-map iframe").length > 0 ) { jQuery(".radar-map iframe").each(function(){ jQuery(this).attr( 'src', function ( i, val ) { return val; }); }); } } else { belchertown_debug("Updating " + section + " images"); // Reload images jQuery('.'+section+' img').each(function(){ new_image_url = jQuery(this).attr('src').split('?')[0] + "?" + Math.floor(Math.random() * 999999999); jQuery(this).attr('src', new_image_url); }); // Reload iframes jQuery('.'+section+' iframe').each(function(){ jQuery(this).attr( 'src', function ( i, val ) { return val; }); }); } // Set the new timer if ( reload_timer_interval_seconds ) { var reload_timer_ms = reload_timer_interval_seconds * 1000; // convert to millis setTimeout(function() { ajaximages(section, reload_timer_interval_seconds); }, reload_timer_ms); } } } var reconnect_using_inactive_timestamp = false; var inactive_timestamp = ""; // MQTT connect function connect() { #if $Extras.has_key("mqtt_websockets_ssl") and $Extras.mqtt_websockets_ssl == '1' belchertown_debug( "MQTT: Connecting to MQTT Websockets: $Extras.mqtt_websockets_host $Extras.mqtt_websockets_port (SSL Enabled)" ); #else belchertown_debug( "MQTT: Connecting to MQTT Websockets: $Extras.mqtt_websockets_host $Extras.mqtt_websockets_port (SSL Disabled)" ); #end if if ( reconnect_using_inactive_timestamp ) { updated = moment.unix( inactive_timestamp ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } else { updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } reported = "$obs.label.mqtt_websockets_connecting $obs.label.header_last_updated " + updated; jQuery(".updated").html( reported ); jQuery(".onlineMarker").hide(); jQuery(".offlineMarker").hide(); jQuery(".loadingMarker").show(); client = new Paho.Client("$Extras.mqtt_websockets_host", $Extras.mqtt_websockets_port, mqttclient); client.onConnectionLost = onConnectionLost; client.onMessageArrived = onMessageArrived; var options = { #if $Extras.has_key("mqtt_websockets_ssl") and $Extras.mqtt_websockets_ssl == '1' useSSL: true, #else useSSL: false, #end if reconnect: true, onSuccess:onConnect, onFailure:onFailure } client.connect( options ); } // MQTT connect callback function onConnect() { mqttConnected = true; belchertown_debug( "MQTT: MQTT Connected. Subscribing." ); if ( reconnect_using_inactive_timestamp ) { updated = moment.unix( inactive_timestamp ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } else { updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } if ( pageName == "pi" ) { reported = "$obs.label.mqtt_websockets_waiting_pi $obs.label.header_last_updated " + updated; } else { reported = "$obs.label.mqtt_websockets_waiting $obs.label.header_last_updated " + updated; } jQuery(".updated").html( reported ); jQuery(".onlineMarker").hide(); jQuery(".offlineMarker").hide(); jQuery(".loadingMarker").show(); client.subscribe( "$Extras.mqtt_websockets_topic" ); #if $Extras.has_key("disconnect_live_website_visitor") and $Extras.disconnect_live_website_visitor != '0' if ( getURLvar("stayconnected") && ( getURLvar("stayconnected") == "true" || getURLvar("stayconnected") == "1" ) ) { belchertown_debug("stayconnected URL var found: ignoring disconnect_live_website_visitor value"); } else { if ( pageName != "pi" ) { var activityTimeout = setTimeout( inactive, $Extras.disconnect_live_website_visitor ); // Stop automatic ajax refresh } } #end if } // MQTT Failure function onFailure() { mqttConnected = false; jQuery(".onlineMarker").hide(); jQuery(".offlineMarker").show(); jQuery(".loadingMarker").hide(); var d = new Date(); epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds if ( client.isConnected() ) { updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } else { updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } jQuery(".updated").html( "$obs.label.mqtt_websockets_failed $obs.label.header_last_updated " + updated ); console.log( "MQTT: " + moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format() + ": Cannot connect to MQTT broker" ); } // MQTT connection lost function onConnectionLost(responseObject) { mqttConnected = false; jQuery(".onlineMarker").hide(); jQuery(".offlineMarker").show(); jQuery(".loadingMarker").hide(); var d = new Date(); epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds if ( client.isConnected() ) { updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } else { updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); } jQuery(".updated").html( "$obs.label.mqtt_websockets_lost $obs.label.header_last_updated " + updated ); if (responseObject.errorCode !== 0) { console.log( "MQTT: " + moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format() + ": mqtt Connection Lost: "+responseObject.errorMessage ); } } function inactive() { client.disconnect(); // Disconnect mqtt belchertown_debug( "MQTT: Inactive timer expired. MQTT Disconnected" ); jQuery(".onlineMarker").hide(); // Hide online beacon jQuery(".offlineMarker").show(); // Show offline beacon jQuery(".loadingMarker").hide(); // Hide loading beacon var d = new Date(); epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" ); jQuery(".updated").html( "$obs.label.mqtt_websockets_stopped $obs.label.header_last_updated " + updated + " " ); reconnect_using_inactive_timestamp = true; // Set a flag to use the inactive timestamp in case we reconnect we have the latest last updated time inactive_timestamp = epoch; // Store this timestamp in case we reconnect } // New message from mqtt, process it function onMessageArrived(message) { belchertown_debug( "MQTT: " + message.payloadString ); update_current_wx( message.payloadString ); } // Handle MQTT message function update_current_wx(data) { data = jQuery.parseJSON( data ); #if $Extras.has_key('googleAnalyticsId') // Send a pageview gtag('config', '$Extras.googleAnalyticsId'); #end if // This message is a weewx archive update. Update weewx data, forecast data and highcharts graphs if ( data.hasOwnProperty("interval_minute") ) { // Delays are recommended to allow the other skins to complete processing belchertown_debug( "MQTT: MQTT message indicates this is an archive interval." ); if ( pageName != "pi" ) { setTimeout( showChart, 30000, homepage_graphgroup ); // Load updated charts. } setTimeout( ajaxweewx, 10000 ); // Update weewx data #if $Extras.has_key("forecast_enabled") and $Extras.forecast_enabled == '1' setTimeout( ajaxforecast, 10000 ); // Update forecast data #end if setTimeout( ajaximages, 10000 ); // Update radar and home page hook "img src" if present } else { // Only show the updated time on non-archive packets epoch = parseFloat( data["dateTime"] ).toFixed(0); updated = moment.unix(epoch).utcOffset($moment_js_utc_offset).format("$obs.label.time_last_updated"); if ( pageName == "pi" ) { updated_text = "$obs.label.mqtt_websockets_connected_pi " + updated; } else { updated_text = "$obs.label.mqtt_websockets_connected " + updated; } jQuery(".updated").html( updated_text ); } // If we're in this function, show the online beacon and hide the others jQuery(".onlineMarker").show(); // Show the online beacon jQuery(".offlineMarker").hide(); jQuery(".loadingMarker").hide(); // Update the station observation box elements station_mqtt_data = Object.keys(data); // Turn data (mqtt message) into an object we can forEach // Get all span elements within the table. This is setup by Python initially jQuery('.station-observations').find("span").each(function () { // The class name is the same as the Extras.station_observations name (weewx schema) thisElementClass = jQuery(this).attr("class") // Loop through each MQTT payload item station_mqtt_data.forEach( mqttdata => { if ( thisElementClass == "rainWithRainRate" ) { // Force dayRain since that's the MQTT payload name thisElementClass = "dayRain"; } if ( thisElementClass == "barometer" || thisElementClass == "pressure" || thisElementClass == "altimeter" || thisElementClass == "cloudbase" ) { // Do not group number into thousands,hundreds format localeStringUseGrouping = false; } else { localeStringUseGrouping = true; } // If this MQTT payload key begins with the name of the span class name, update the info. Can also use mqttdata.includes(thisElementClass) if weewx-mqtt changes in future if ( mqttdata.startsWith(thisElementClass) ) { html_output = parseFloat(parseFloat(data[mqttdata])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array[thisElementClass], maximumFractionDigits: unit_rounding_array[thisElementClass], useGrouping: localeStringUseGrouping}) + unit_label_array[thisElementClass]; // Finally update the element class jQuery("." + thisElementClass).html( html_output ); } }); }); // End dynamic station observation box // Temperature F if ( data.hasOwnProperty("outTemp_F") ) { // Inside parseFloat converts str to int. Outside parseFloat processes the locale string // Help from: https://stackoverflow.com/a/40152286/1177153 and https://stackoverflow.com/a/31581206/1177153 outTemp = parseFloat(parseFloat(data["outTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}); get_outTemp_color( "degree_F", outTemp ); jQuery(".outtemp").html( outTemp ); // Feels like temp as defined by NOAA's "Apparent Temperature" at: http://www.nws.noaa.gov/ndfd/definitions.htm //if ( data["outTemp_F"] <= 50 ) { // jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["windchill_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" ); //} else if ( data["outTemp_F"] >= 80 ) { // jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["heatindex_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" ); //} else { // jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["outTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" ); //} } // Temperature C if ( data.hasOwnProperty("outTemp_C") ) { outTemp = parseFloat(parseFloat(data["outTemp_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}); get_outTemp_color( "degree_C", outTemp ); jQuery(".outtemp").html( outTemp ); } // Apparent Temperature US if ( data.hasOwnProperty("appTemp_F") ) { jQuery(".feelslike").html( "$obs.label.feels_like: " + parseFloat(parseFloat(data["appTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}) + " $unit.label.outTemp" ); } // Apparent Temperature Metric if ( data.hasOwnProperty("appTemp_C") ) { jQuery(".feelslike").html( "$obs.label.feels_like: " + parseFloat(parseFloat(data["appTemp_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}) + " $unit.label.outTemp" ); } // Wind if ( data.hasOwnProperty("windDir") ) { // No toLocaleString() here since there is no float decimal needed. rotateThis( data["windDir"] ); //jQuery(".wind-arrow").css( "transform", "rotate(" + data["windDir"] + "deg)" ); jQuery(".curwinddeg").html( parseFloat( data["windDir"] ).toFixed(0) + "°" ); jQuery(".curwinddir").html( highcharts_tooltip_factory( parseFloat( data["windDir"] ).toFixed(0), "windDir" ) ); } // Windspeed US if ( data.hasOwnProperty("windSpeed_mph") ) { jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_mph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) ); } // Windspeed Metric if (data.hasOwnProperty("windSpeed_kph")) { windSpeed = parseFloat(parseFloat(data["windSpeed_kph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}); get_windSpeed_color(windSpeed); jQuery(".curwindspeed").html(windSpeed); } // Windspeed METRICWX 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"]}) ); } // Wind Gust US // May not be provided in mqtt, but just in case. if ( data.hasOwnProperty("windGust_mph") ) { jQuery(".curwindgust").html( parseFloat(parseFloat(data["windGust_mph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]}) ); } // Wind Gust Metric if (data.hasOwnProperty("windGust_kph")) { windGust = parseFloat(parseFloat(data["windGust_kph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]}); get_windGust_color(windSpeed); jQuery(".curwindgust").html(windGust); // Wind Gust METRICWX 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"]}) ); } // Windchill US if ( data.hasOwnProperty("windchill") ) { jQuery(".curwindchill").html( parseFloat(parseFloat(data["windchill"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windchill"], maximumFractionDigits: unit_rounding_array["windchill"]}) + "$unit.label.outTemp" ); } // Windchill Metric if ( data.hasOwnProperty("windchill_C") ) { jQuery(".curwindchill").html( parseFloat(parseFloat(data["windchill_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windchill"], maximumFractionDigits: unit_rounding_array["windchill"]}) + "$unit.label.outTemp" ); } }; #end if Highcharts.setOptions({ global: { //useUTC: false timezoneOffset: $highcharts_timezoneoffset }, lang: { decimalPoint: "$highcharts_decimal", thousandsSep: "$highcharts_thousands" } }); function showChart(json_file, prepend_renderTo=false) { // Relative URL by finding what page we're on currently. jQuery.getJSON(get_relative_url() + '/json/' + json_file + '.json', function(data) { // Loop through each chart name (e.g. chart1, chart2, chart3) jQuery.each(data, function (plotname, obsname) { var observation_type = undefined; // Ignore the Belchertown Version since this "plot" has no other options if ( plotname == "belchertown_version" ) { return true; } // Ignore the generated timestamp since this "plot" has no other options if ( plotname == "generated_timestamp" ) { return true; } // Ignore the chartgroup_title since this "plot" has no other options if ( plotname == "chartgroup_title" ) { return true; } // Set the chart's tooltip date time format, then return since this "plot" has no other options if ( plotname == "tooltip_date_format" ) { tooltip_date_format = obsname; return true; } // Set the chart colors, then return right away since this "plot" has no other options if ( plotname == "colors" ) { colors = obsname.split(","); return true; } // Set the chart credits, then return right away since this "plot" has no other options if ( plotname == "credits" ) { credits = obsname.split(",")[0]; return true; } // Set the chart credits url, then return right away since this "plot" has no other options if ( plotname == "credits_url" ) { credits_url = obsname.split(",")[0]; return true; } // Set the chart credits position, then return right away since this "plot" has no other options if ( plotname == "credits_position" ) { credits_position = obsname; return true; } // Loop through each chart options jQuery.each(data[plotname]["options"], function (optionName, optionVal) { switch(optionName) { case "type": type = optionVal; break; case "renderTo": renderTo = optionVal; break; case "title": title = optionVal; break; case "subtitle": subtitle = optionVal; break; case "yAxis_label": yAxis_label = optionVal; break; case "chart_group": chart_group = optionVal; break; case "gapsize": gapsize = optionVal; break; case "connectNulls": connectNulls = optionVal; break; case "rounding": rounding = optionVal; break; case "xAxis_categories": xAxis_categories = optionVal; break; case "plot_tooltip_date_format": plot_tooltip_date_format = optionVal; break; case "css_class": css_class = optionVal; break; case "css_height": css_height = optionVal; break; case "css_width": css_width = optionVal; break; case "legend": legend_enabled = optionVal; break; case "exporting": exporting_enabled = optionVal; break; } }); // Handle any per-chart date time format override if ( typeof plot_tooltip_date_format !== "undefined" ) { var tooltip_date_format = plot_tooltip_date_format; } var options = { chart: { renderTo: '', spacing: [5, 10, 10, 0], type: '', zoomType: 'x' }, exporting: { enabled: JSON.parse( String( exporting_enabled ) ) // Convert string to bool }, title: { useHTML: true, text: '' }, subtitle: { text: '' }, legend: { enabled: JSON.parse( String( legend_enabled ) ) // Convert string to bool }, xAxis: { dateTimeLabelFormats: { day: '%e %b', week: '%e %b', month: '%b %y', }, lineColor: '#555', minRange: 900000, minTickInterval: 900000, title: { style: { font: 'bold 12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' } }, ordinal: false, type: 'datetime' }, yAxis: [{ endOnTick: true, lineColor: '#555', minorGridLineWidth: 0, startOnTick: true, showLastLabel: true, title: { }, opposite: false }], plotOptions: { area: { lineWidth: 2, gapSize: '', gapUnit: 'value', marker: { enabled: false, radius: 2 }, threshold: null, softThreshold: true }, line: { lineWidth: 2, gapSize: '', gapUnit: 'value', marker: { enabled: false, radius: 2 }, }, spline: { lineWidth: 2, gapSize: '', gapUnit: 'value', marker: { enabled: false, radius: 2 }, }, areaspline: { lineWidth: 2, gapSize: '', gapUnit: 'value', marker: { enabled: false, radius: 2 }, threshold: null, softThreshold: true }, scatter: { gapSize: '', gapUnit: 'value', marker: { radius: 2 }, }, }, // Highstock is needed for gapsize. Disable these 3 to make it look like standard Highcharts scrollbar: { enabled: false }, navigator: { enabled: false }, rangeSelector: { enabled: false }, tooltip: { enabled: true, crosshairs: true, dateTimeLabelFormats: { hour: '%e %b %H:%M' }, // For locale control with moment.js formatter: function (tooltip) { try { // The first returned item is the header, subsequent items are the points. // Mostly applies to line style charts (line, spline, area) return [moment.unix( this.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format )].concat( this.points.map(function (point) { // If observation_type is in the series array, use that otherwise use the obsType var point_obsType = point.series.userOptions.observation_type ? point.series.userOptions.observation_type : point.series.userOptions.obsType; var rounding = point.series.userOptions.rounding; var mirrored = point.series.userOptions.mirrored_value; var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : ""; return "\u25CF " + point.series.name + ': ' + highcharts_tooltip_factory( point.y, point_obsType, true, rounding, mirrored, numberFormat ); }) ); } catch(e) { // There's an error so check if it's windDir to apply wind direction label, or if it's a scatter. If none of those revert back to default tooltip. if (this.series.userOptions.obsType == "windDir" || this.series.userOptions.observation_type == "windDir") { // If observation_type is in the series array, use that otherwise use the obsType var point_obsType = this.series.userOptions.observation_type ? this.series.userOptions.observation_type : this.series.userOptions.obsType; var rounding = this.series.userOptions.rounding; var mirrored = this.series.userOptions.mirrored_value; return moment.unix( this.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) +'
' + highcharts_tooltip_factory( this.point.y, point_obsType, true, rounding, mirrored ); } else if (this.series.userOptions.type == "scatter") { // Catch anything else that might be a scatter plot. Scatter plots will just show x,y coordinates without this. return "\u25CF " + this.series.name + ': ' + Highcharts.numberFormat(this.y); } else { return tooltip.defaultFormatter.call(this, tooltip); } } }, split: true, }, credits: {}, series: [{}] }; // Default options completed, build overrides from JSON and graphs.conf // Set the chart render div and title if ( prepend_renderTo ) { options.chart.renderTo = json_file + "_" + renderTo; } else { options.chart.renderTo = renderTo; } belchertown_debug( options.chart.renderTo + ": building a " + type + " chart" ); if ( css_class ) { jQuery( "#" + options.chart.renderTo ).addClass( css_class ); belchertown_debug( options.chart.renderTo + ": div id is " + options.chart.renderTo + " and adding CSS class: " + css_class ); } options.chart.type = type; options.title.text = ""+title+""; // Anchor link to chart for direct linking options.subtitle.text = subtitle; options.plotOptions.area.gapSize = gapsize; options.plotOptions.line.gapSize = gapsize; options.plotOptions.spline.gapSize = gapsize; options.plotOptions.scatter.gapSize = gapsize; if ( connectNulls == "true" ) { options.plotOptions.series = { connectNulls: connectNulls }; } options.colors = colors; // If we have xAxis categories, reset xAxis and populate it from these options. Also need to reset tooltip since there's no datetime for moment.js to use. if ( xAxis_categories.length >= 1 ) { belchertown_debug( options.chart.renderTo + ": has " + xAxis_categories.length + " xAxis categories. Resetting xAxis and tooltips for grouping" ); options.xAxis = {} options.xAxis.categories = xAxis_categories; options.tooltip = {} options.tooltip = { enabled: true, crosshairs: true, split: true, formatter: function () { // The first returned item is the header, subsequent items are the points return [this.x].concat( this.points.map(function (point) { // If observation_type is in the series array, use that otherwise use the obsType var point_obsType = point.series.userOptions.observation_type ? point.series.userOptions.observation_type : point.series.userOptions.obsType; var rounding = point.series.userOptions.rounding; var mirrored = point.series.userOptions.mirrored_value; return "\u25CF " + point.series.name + ': ' + highcharts_tooltip_factory( point.y, point_obsType, true, rounding, mirrored ); }) ); }, } } // Reset the series everytime we loop. options.series = []; // Build the series var i = 0; jQuery.each(data[plotname]["series"], function (seriesName, seriesVal) { observation_type = data[plotname]["series"][seriesName]["obsType"]; options.series[i] = data[plotname]["series"][seriesName]; i++; }); /* yAxis customization handler and label handling Take the following example. yAxis is in observation 0 (rainTotal), so that label is caught and set by yAxis1_active. If you move yAxis to observation 1 (rainRate), then the label is caught and set by yAxis_index. There may be a more efficient way to do this. If so, please submit a pull request :) [[[chart3]]] title = Rain [[[[rainTotal]]]] name = Rain Total yAxis = 1 [[[[rainRate]]]] */ var yAxis1_active = undefined; // Find if any series have yAxis = 1. If so, save the array number so we can set labels correctly. // We really care if yAxis is in array 1+, so we can go back and set yAxis 0 to the right label. var yAxis_index = options.series.findIndex( function(item){ return item.yAxis == 1 } ) // Handle series specific data, overrides and non-Highcharts options that we passed through options.series.forEach(s => { if (s.yAxis == "1") { // If yAxis = 1 is set for the observation, add a new yAxis and associate that observation to the right side of the chart yAxis1_active = true; options.yAxis.push({ // Secondary yAxis opposite: true, title: { text: s.yAxis_label, }, }), // Associate this series to the new yAxis 1 s.yAxis = 1 // We may have already passed through array 0 in the series without setting the "multi axis label", go back and explicitly define it. if ( yAxis_index >= 1 ) { options.yAxis[0].title.text = options.series[0].yAxis_label; } } else { if ( yAxis1_active ) { // This yAxis is first in the data series, so we can set labels without needing to double back options.yAxis[0].title.text = s.yAxis_label; } else { // Apply the normal yAxis 0's label without observation name options.yAxis[0].title.text = s.yAxis_label; } // Associate this series to yAxis 1 s.yAxis = 0; } // Run yAxis customizations this_yAxis = s.yAxis; belchertown_debug( options.chart.renderTo + ": " + s.obsType + " is on yAxis " + this_yAxis ); // Some charts may require a defined min/max on the yAxis options.yAxis[this_yAxis].min = s.yAxis_min !== "undefined" ? s.yAxis_min : null; options.yAxis[this_yAxis].max = s.yAxis_max !== "undefined" ? s.yAxis_max : null; // Some charts may require a defined soft min/max on the yAxis options.yAxis[this_yAxis].softMin = s.yAxis_softMin !== "undefined" ? parseInt(s.yAxis_softMin) : null; options.yAxis[this_yAxis].softMax = s.yAxis_softMax !== "undefined" ? parseInt(s.yAxis_softMax) : null; // Set the yAxis tick interval. Mostly used for barometer. if ( s.yAxis_tickInterval ) { options.yAxis[this_yAxis].tickInterval = s.yAxis_tickInterval; } // Set yAxis minorTicks. This is a graph-wide setting so setting it for any of the yAxis will set it for the graph itself if ( s.yAxis_minorTicks ) { options.yAxis[this_yAxis].minorTicks = true; } // 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}' } } else { options.yAxis[this_yAxis].labels = { format: '{value:.2f}' } } } // Rain, RainRate and rainTotal (special Belchertown skin observation) get yAxis precision if (s.obsType == "rain" || s.obsType == "rainRate" || s.obsType == "rainTotal") { options.yAxis[this_yAxis].min = 0; options.yAxis[this_yAxis].minRange = 0.01; options.yAxis[this_yAxis].minorGridLineWidth = 1; } if (s.obsType == "windDir") { options.yAxis[this_yAxis].tickInterval = 90; options.yAxis[this_yAxis].labels = { formatter: function() { var value = weatherdirection[this.value]; return value !== 'undefined' ? value : this.value; } } } // Check if this series has a gapsize override if (s.gapsize) { options.plotOptions.area.gapSize = s.gapsize; options.plotOptions.line.gapSize = s.gapsize; options.plotOptions.spline.gapSize = s.gapsize; options.plotOptions.scatter.gapSize = s.gapsize; } // If this chart is a mirrored chart, make the yAxis labels non-negative if ( s.mirrored_value ) { belchertown_debug( options.chart.renderTo + ": mirrored chart due to mirrored_value = true" ); options.yAxis[s.yAxis].labels = { formatter: function() { return Math.abs(this.value); } } } // Lastly, apply any numberFormat label overrides if ( typeof s.numberFormat !== "undefined" && Object.keys(s.numberFormat).length >= 1 ) { var { decimals, decimalPoint, thousandsSep } = s.numberFormat options.yAxis[this_yAxis].labels = { formatter: function () { return Highcharts.numberFormat(this.value, decimals, decimalPoint, thousandsSep); } } } }); // If windRose is present, configure a special chart to show that data if (observation_type == "windRose") { #if isinstance($ordinate_names[1], str) is True: ## Python 3 unicode string, don't change anything var categories = $ordinate_names; #else ## Python 2 hack to convert backslash unicode to html char var categories = #echo [x.encode('ascii', 'xmlcharrefreplace') for x in $ordinate_names] #; #end if options.chart.className = "highcharts-windRose"; // Used for dark mode options.chart.type = "column"; options.chart.polar = true; options.chart.alignTicks = false; options.pane = { size: '80%' } // Reset xAxis and rebuild options.xAxis = {} options.xAxis.min = 0; options.xAxis.max = 16; options.xAxis.crosshair = true; options.xAxis.categories = categories; options.xAxis.tickmarkPlacement = 'on'; //options.xAxis.labels = { useHTML: true } // Option disabled in highcharts 8 //options.legend.align = "right"; options.legend.verticalAlign = "top"; options.legend.x = 210; options.legend.y = 119; options.legend.layout = "vertical"; options.legend.floating = true; options.yAxis[0].min = 0; options.yAxis[0].endOnTick = false; options.yAxis[0].reversedStacks = false; options.yAxis[0].title.text = "$obs.label.graphs_windrose_frequency (%)"; options.yAxis[0].gridLineWidth = 0; options.yAxis[0].labels = { enabled: false } options.yAxis[0].zIndex = 800; options.plotOptions = { column: { stacking: 'normal', shadow: false, groupPadding: 0, pointPlacement: 'on', } } // Reset the tooltip options.tooltip = {} options.tooltip.shared = true; options.tooltip.valueSuffix = '%'; options.tooltip.followPointer = true; options.tooltip.useHTML = true; // Since wind rose is a special observation, I did not re-do the JSON arrays to accomodate it as a separate array. // So we need to grab the data array within the series and save it to a temporary array, delete the entire chart series, // and reapply the windrose data back to the series. var newSeries = options.series[0].data; options.series = []; newSeries.forEach( ns => { options.series.push(ns); }); } // If Hays chart is present, configure a special chart to show that data if (observation_type == "haysChart") { options.chart.type = "arearange" options.chart.polar = true; options.plotOptions = { turboThreshold: 0, series: { marker: { enabled: 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, tickmarkPlacement: 'on', min: -1, softMax: range_max, title: { text: options.series[0].yAxis_label, }, labels: { align: 'center', x: 0, y: 0 }, } options.tooltip = { split: false, shared: true, followPointer: true, useHTML: true, formatter: function (tooltip) { return this.points.map(function (point) { var rounding = point.series.userOptions.rounding; var mirrored = point.series.userOptions.mirrored_value; var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : ""; return "" + moment.unix( point.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) + "
\u25CF $obs.label.highest_temperature: " + highcharts_tooltip_factory( point.point.high, observation_type, true, rounding, mirrored, numberFormat ) + "
\u25CF $obs.label.lowest_temperature: " + highcharts_tooltip_factory( point.point.low, observation_type, true, rounding, mirrored, numberFormat ); }); } } var currentSeries = options.series; var currentSeriesData = options.series[0].data; var range_unit = options.series[0].range_unit; var newSeriesData = []; var currentSeriesColor = options.series[0].color; currentSeriesData.forEach( seriesData => { newSeriesData.push({ x: seriesData[0], low: seriesData[1], high: seriesData[2], }); }); options.series = []; options.series.push({ data: newSeriesData, obsType: "haysChart", obsUnit: range_unit, color: currentSeriesColor, fillColor: currentSeriesColor, connectEnds: false, }); } // If weather range is present, configure a special chart to show that data // https://www.highcharts.com/blog/tutorials/209-the-art-of-the-chart-weather-radials/ if (observation_type == "weatherRange") { options.chart.type = "columnrange"; // If polar is defined, use it and add a special dark mode CSS class if ( JSON.parse( String( options.series[0].polar.toLowerCase() ) ) ) { options.chart.polar = true; // Make sure the option is a string, then convert to bool options.chart.className = "highcharts-weatherRange belchertown-polar"; // Used for dark mode } else { options.chart.className = "highcharts-weatherRange"; // Used for dark mode } options.legend = { "enabled": false } // Find min and max of the series data for the yAxis min and max var minimum_flattened = []; var maximum_flattened = []; options.series[0].data.forEach( seriesData => { minimum_flattened.push(seriesData[1]); maximum_flattened.push(seriesData[2]); }); var range_min = Math.min(...minimum_flattened); var range_max = Math.max(...maximum_flattened); var yAxis_tickInterval = Math.ceil( Math.round(range_max / 5) / 5 ) * 5; // Divide max outTemp by 5 and round it, then round that value up to the nearest 5th multiple. This gives clean yAxis tick lines. options.yAxis = { showFirstLabel: true, tickInterval: yAxis_tickInterval, min: range_min, max: range_max, title: { text: options.series[0].yAxis_label, }, } options.xAxis = {} options.xAxis = { labels: { format: "{value: %b}" }, tickInterval: 2592000000, // 30 days showLastLabel: true, crosshair: true, type: "datetime" } options.plotOptions = {} options.plotOptions = { series: { turboThreshold: 0, stacking: "normal", showInLegend: false, borderWidth: 0, } } options.tooltip = { split: false, shared: true, followPointer: true, useHTML: true, formatter: function (tooltip) { return this.points.map(function (point) { var rounding = point.series.userOptions.rounding; var mirrored = point.series.userOptions.mirrored_value; var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : ""; return "" + moment.unix( point.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) + "
\u25CF $obs.label.highest_temperature: " + highcharts_tooltip_factory( point.point.high, observation_type, true, rounding, mirrored, numberFormat ) + "
\u25CF $obs.label.lowest_temperature: " + highcharts_tooltip_factory( point.point.low, observation_type, true, rounding, mirrored, numberFormat ) + "
\u25CF $obs.label.average_temperature: " + highcharts_tooltip_factory( point.point.average, observation_type, true, rounding, mirrored, numberFormat ); }); } } // Update data var currentSeries = options.series; var currentSeriesData = options.series[0].data; var range_unit = options.series[0].range_unit; var newSeriesData = []; currentSeriesData.forEach( seriesData => { if ( options.series[0].color ) { var color = options.series[0].color; } else { // Set color of the column based on the average temperature, or return default if not temperature var color = get_outTemp_color( range_unit, seriesData[3], true ); // Set color of the column based on the Windspeed, or return default if not temperature var color = get_windspeed_color( range_unit, seriesData[3], true ); } newSeriesData.push({ x: seriesData[0], low: seriesData[1], high: seriesData[2], average: seriesData[3], color: color }); }); options.series = []; options.series.push({ data: newSeriesData, obsType: "weatherRange", obsUnit: range_unit }); } // Apply any width, height CSS overrides to the parent div of the chart if ( css_height != "" ) { jQuery("#"+options.chart.renderTo).parent().css({ 'height' : css_height, 'padding' : '0px 15px', 'margin-bottom' : '20px' }); } if ( css_width != "" ) { jQuery("#"+options.chart.renderTo).parent().css('width', css_width); } if ( credits != "highcharts_default" ) { options.credits.text = credits; } if ( credits_url != "highcharts_default" ) { options.credits.href = credits_url; } if ( credits_position != "highcharts_default" ) { options.credits.position = JSON.parse(credits_position); } // Finally all options are done, now show the chart var chart = new Highcharts.chart(options); // If using debug, show a copy paste debug for use with jsfiddle belchertown_debug("Highcharts.chart('container', " + JSON.stringify(options) + ");"); }); }); };