diff --git a/README.md b/README.md index 76b3f7b..c30a855 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Any messages that are over 160 characters are chunked into 160 message bytes to - Other functions - `whereami` returns the address of location of sender if known - `tide` returns the local tides, NOAA data source - - `wx` and `wxc` returns local weather forecast, NOAA data source (wxc is metric value) + - `wx` and `wxc` returns local weather forecast, (wxc is metric value), NOAA or Open Meteo for weather forcasting. - `wxa` and `wxalert` return NOAA alerts. Short title or expanded details - `joke` tells a joke - `messages` Replay the last messages heard, like Store and Forward @@ -80,6 +80,14 @@ Setting the default channel is the channel that won't be spammed by the bot. It' respond_by_dm_only = True defaultChannel = 0 ``` +The weather forcasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use. +``` +[location] +enabled = True +lat = 48.50 +lon = -123.0 +UseMeteoWxAPI = True +``` Modules can be disabled or enabled. ``` @@ -132,6 +140,7 @@ pip install geopy pip install maidenhead pip install beautifulsoup4 pip install dadjokes +pip install openmeteo_requests ``` To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji` diff --git a/config.template b/config.template index 6168d84..48749f6 100644 --- a/config.template +++ b/config.template @@ -54,10 +54,12 @@ bbs_admin_list = enabled = True lat = 48.50 lon = -123.0 -# weather forecast days, the first two rows are today and tonight -DAYS_OF_WEATHER = 4 +# NOAA weather forecast days, the first two rows are today and tonight +NOAAforecastDuration = 4 # number of weather alerts to display -ALERT_COUNT = 2 +NOAAalertCount = 2 +# use Open-Meteo API for weather data not NOAA usefull for non US locations +UseMeteoWxAPI = False # solar module [solar] diff --git a/mesh_bot.py b/mesh_bot.py index d251055..e61e569 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -82,21 +82,26 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi location = get_node_location(message_from_id, deviceID, channel_number) moon = get_moon(str(location[0]),str(location[1])) bot_response = moon - elif "wxalert" in message.lower(): - location = get_node_location(message_from_id, deviceID, channel_number) - weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1])) - bot_response = weatherAlert - elif "wxa" in message.lower(): - location = get_node_location(message_from_id, deviceID, channel_number) - weatherAlert = getWeatherAlerts(str(location[0]),str(location[1])) - bot_response = weatherAlert + elif "wxalert" in message.lower() or "wxa" in message.lower(): + if use_meteo_wxApi: + bot_response = "wxalert is not supported" + else: + location = get_node_location(message_from_id, deviceID) + weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1])) + bot_response = weatherAlert elif "wxc" in message.lower(): - location = get_node_location(message_from_id, deviceID, channel_number) - weather = get_weather(str(location[0]),str(location[1]),1) + location = get_node_location(message_from_id, deviceID) + if use_meteo_wxApi: + weather = get_wx_meteo(str(location[0]),str(location[1]),1) + else: + weather = get_weather(str(location[0]),str(location[1]),1) bot_response = weather elif "wx" in message.lower(): - location = get_node_location(message_from_id, deviceID, channel_number) - weather = get_weather(str(location[0]),str(location[1])) + location = get_node_location(message_from_id, deviceID) + if use_meteo_wxApi: + weather = get_wx_meteo(str(location[0]),str(location[1])) + else: + weather = get_weather(str(location[0]),str(location[1])) bot_response = weather elif "joke" in message.lower(): bot_response = tell_joke() @@ -339,7 +344,10 @@ async def start_rx(): if solar_conditions_enabled: logger.debug(f"System: Celestial Telemetry Enabled") if location_enabled: - logger.debug(f"System: Location Telemetry Enabled") + if use_meteo_wxApi: + logger.debug(f"System: Location Telemetry Enabled using Open-Meteo API") + else: + logger.debug(f"System: Location Telemetry Enabled using NOAA API") if dad_jokes_enabled: logger.debug(f"System: Dad Jokes Enabled!") if store_forward_enabled: diff --git a/modules/settings.py b/modules/settings.py index 7c3b24a..14cbe2b 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -64,6 +64,7 @@ try: location_enabled = config['location'].getboolean('enabled', False) latitudeValue = config['location'].getfloat('lat', 48.50) longitudeValue = config['location'].getfloat('lon', -123.0) + use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA zuluTime = config['general'].getboolean('zuluTime', False) welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG) welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message @@ -75,8 +76,8 @@ try: log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True config['general'].get('motd', MOTD) urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds - forecastDuration = config['general'].getint('DAYS_OF_WEATHER', 4) # default days of weather - numWxAlerts = config['general'].getint('ALERT_COUNT', 2) # default 2 alerts + forecastDuration = config['general'].getint('NOAAforecastDuration', 4) # NOAA forcast days + numWxAlerts = config['general'].getint('NOAAalertCount', 2) # default 2 alerts bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',') bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',') repeater_enabled = config['repeater'].getboolean('enabled', False) diff --git a/modules/system.py b/modules/system.py index 433ba5f..a1ca781 100644 --- a/modules/system.py +++ b/modules/system.py @@ -29,13 +29,20 @@ if sitrep_enabled: if solar_conditions_enabled: from modules.solarconditions import * # from the spudgunman/meshing-around repo trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon - help_message = help_message + ", sun, hfcond, solar, moon, tide" + help_message = help_message + ", sun, hfcond, solar, moon" # Location Configuration if location_enabled: from modules.locationdata import * # from the spudgunman/meshing-around repo trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx - help_message = help_message + ", whereami, wx, wxc, wxa" + help_message = help_message + ", whereami, wx, wxc" + + # Open-Meteo Configuration for worldwide weather + if use_meteo_wxApi: + from modules.wx_meteo import * # from the spudgunman/meshing-around repo + else: + # NOAA only features + help_message = help_message + ", wxa, tide" # BBS Configuration if bbs_enabled: @@ -192,18 +199,23 @@ def get_node_list(nodeInt=1): node_list2.sort(key=lambda x: x[1], reverse=True) except Exception as e: logger.error(f"System: Error sorting node list: {e}") - print (f"Node List1: {node_list1[:5]}\n") - print (f"Node List2: {node_list2[:5]}\n") + node_list = ERROR_FETCHING_DATA + #print (f"Node List1: {node_list1[:5]}\n") + #print (f"Node List2: {node_list2[:5]}\n") - # make a nice list for the user - for x in node_list1[:SITREP_NODE_COUNT]: - short_node_list.append(f"{x[0]} SNR:{x[2]}") - for x in node_list2[:SITREP_NODE_COUNT]: - short_node_list.append(f"{x[0]} SNR:{x[2]}") + try: + # make a nice list for the user + for x in node_list1[:SITREP_NODE_COUNT]: + short_node_list.append(f"{x[0]} SNR:{x[2]}") + for x in node_list2[:SITREP_NODE_COUNT]: + short_node_list.append(f"{x[0]} SNR:{x[2]}") - for x in short_node_list: - if x != "" or x != '\n': - node_list += x + "\n" + for x in short_node_list: + if x != "" or x != '\n': + node_list += x + "\n" + except Exception as e: + logger.error(f"System: Error creating node list: {e}") + node_list = ERROR_FETCHING_DATA return node_list diff --git a/modules/wx_meteo.py b/modules/wx_meteo.py new file mode 100644 index 0000000..8e403b8 --- /dev/null +++ b/modules/wx_meteo.py @@ -0,0 +1,167 @@ +import openmeteo_requests # pip install openmeteo-requests +from retry_requests import retry +#import requests_cache +from modules.log import * + +def get_wx_meteo(lat=0, lon=0, unit=0): + # set forcast days 1 or 3 + forecastDays = 3 + + # Setup the Open-Meteo API client with cache and retry on error + #cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) + #retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) + retry_session = retry(retries = 3, backoff_factor = 0.2) + openmeteo = openmeteo_requests.Client(session = retry_session) + + # Make sure all required weather variables are listed here + # The order of variables in hourly or daily is important to assign them correctly below + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": {lat}, + "longitude": {lon}, + "daily": ["weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_hours", "precipitation_probability_max", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant"], + "timezone": "auto", + "forecast_days": {forecastDays} + } + + # Unit 0 is imperial, 1 is metric + if unit == 0: + params["temperature_unit"] = "fahrenheit" + params["wind_speed_unit"] = "mph" + params["precipitation_unit"] = "inch" + params["distance_unit"] = "mile" + params["pressure_unit"] = "inHg" + + responses = openmeteo.weather_api(url, params=params) + + # Process first location. Add a for-loop for multiple locations or weather models + response = responses[0] + logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}") + + # Process daily data. The order of variables needs to be the same as requested. + daily = response.Daily() + daily_weather_code = daily.Variables(0).ValuesAsNumpy() + daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy() + daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy() + daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy() + daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy() + daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy() + daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy() + daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy() + + # convert wind value to cardinal directions + for value in daily_wind_direction_10m_dominant: + if value < 22.5: + wind_direction = "N" + elif value < 67.5: + wind_direction = "NE" + elif value < 112.5: + wind_direction = "E" + elif value < 157.5: + wind_direction = "SE" + elif value < 202.5: + wind_direction = "S" + elif value < 247.5: + wind_direction = "SW" + elif value < 292.5: + wind_direction = "W" + elif value < 337.5: + wind_direction = "NW" + else: + wind_direction = "N" + + # create a weather report + weather_report = "" + for i in range(forecastDays): + if str(i + 1) == "1": + weather_report += "Today, " + elif str(i + 1) == "2": + weather_report += "Tomorrow, " + else: + weather_report += "Futurecast: " + + # report weather from WMO Weather interpretation codes (WW) + code_string = "" + if daily_weather_code[i] == 0: + code_string = "Clear sky" + elif daily_weather_code[i] == 1 or 2 or 3: + code_string = "Partly cloudy" + elif daily_weather_code[i] == 45 or 48: + code_string = "Fog" + elif daily_weather_code[i] == 51: + code_string = "Drizzle: Light" + elif daily_weather_code[i] == 53: + code_string = "Drizzle: Moderate" + elif daily_weather_code[i] == 55: + code_string = "Drizzle: Heavy" + elif daily_weather_code[i] == 56: + code_string = "Freezing Drizzle: Light" + elif daily_weather_code[i] == 57: + code_string = "Freezing Drizzle: Moderate" + elif daily_weather_code[i] == 61: + code_string = "Rain: Slight" + elif daily_weather_code[i] == 63: + code_string = "Rain: Moderate" + elif daily_weather_code[i] == 65: + code_string = "Rain: Heavy" + elif daily_weather_code[i] == 66: + code_string = "Freezing Rain: Light" + elif daily_weather_code[i] == 67: + code_string = "Freezing Rain: Dense" + elif daily_weather_code[i] == 71: + code_string = "Snow: Light" + elif daily_weather_code[i] == 73: + code_string = "Snow: Moderate" + elif daily_weather_code[i] == 75: + code_string = "Snow: Heavy" + elif daily_weather_code[i] == 77: + code_string = "Snow Grains" + elif daily_weather_code[i] == 80: + code_string = "Rain showers: Slight" + elif daily_weather_code[i] == 81: + code_string = "Rain showers: Moderate" + elif daily_weather_code[i] == 82: + code_string = "Rain showers: Heavy" + elif daily_weather_code[i] == 85: + code_string = "Snow showers: Light" + elif daily_weather_code[i] == 86: + code_string = "Snow showers: Moderate" + elif daily_weather_code[i] == 95: + code_string = "Thunderstorm: Slight" + elif daily_weather_code[i] == 96: + code_string = "Thunderstorm: Moderate" + elif daily_weather_code[i] == 99: + code_string = "Thunderstorm: Heavy" + + weather_report += "Cond: " + code_string + ". " + + # report temperature + if unit == 0: + weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "F, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "F. " + else: + weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "C, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "C. " + + # check for precipitation + if daily_precipitation_hours[i] > 0: + if unit == 0: + weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "in, in " + str(round(daily_precipitation_hours[i],2)) + " hours. " + else: + weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "mm, in " + str(round(daily_precipitation_hours[i],2)) + " hours. " + else: + weather_report += "No Precip. " + + # check for wind + if daily_wind_speed_10m_max[i] > 0: + if unit == 0: + weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "mph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "mph from:" + wind_direction + "." + else: + weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "kph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "kph from:" + wind_direction + "." + else: + weather_report += "No Wind\n" + + # add a new line for the next day + if i < forecastDays - 1: + weather_report += "\n" + + return weather_report + diff --git a/requirements.txt b/requirements.txt index 370a21d..cd19afd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ geopy maidenhead beautifulsoup4 dadjokes +openmeteo_requests