diff --git a/README.md b/README.md index 26de5f0..2a81462 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ ![](img/wind.png) -MeshBot Weather is a spinoff of [MeshBot](https://github.com/868meshbot/meshbot) with a detailed focus on weather. Designed to run on a computer or a -Raspberry Pi with a connected Meshtastic device. +[MeshBot Weather](https://github.com/oasis6212/Meshbot_weather) is a spinoff of [MeshBot](https://github.com/868meshbot/meshbot) that brings you +accurate, real-time forecasts and instant weather alerts. Designed to run on a computer or a +Raspberry Pi with a connected Meshtastic radio. Our Mission: @@ -62,9 +63,10 @@ Our Mission: - Forecasts are generated for any location. Not limited to towns or cities. - Optional firewall, when enabled, the bot will only respond to messages from nodes that have been included in its whitelist. +![](img/automaticgrid.png) -![](img/Newfeatures.png) +![](img/newfeatures1.png) Thanks [davidfries](https://github.com/davidfries)! @@ -182,6 +184,16 @@ cd meshbot_weather See above under "How to run the program on various operating systems." +## Location setup for alerts and forecast +You will need to edit the settings.yaml file. Look for: + +ALERT_LAT: "37.7654" + +ALERT_LON: "-100.0151" + +Change these coordinates to match the location you want weather info and alerts for. Do not use more than four digits +past the decimal point. + ## Configuration The ''settings.yaml'' file; it's where you can configure different options. Can be edited in notepad. @@ -195,18 +207,19 @@ MYNODES: FIREWALL: false DM_MODE: true DUTYCYCLE: false -NWS_OFFICE: "BGM" -NWS_GRID_X: "84" -NWS_GRID_Y: "89" ALERT_LAT: "37.7654" ALERT_LON: "-100.0151" +NWS_OFFICE: "" +NWS_GRID_X: "" +NWS_GRID_Y: "" ALERT_CHECK_INTERVAL: 300 ALERT_INCLUDE_DESCRIPTION: ALERT_CHANNEL_INDEX: 0 FIRST_MESSAGE_DELAY: 0 MESSAGE_DELAY: 15 -ENABLE_FULL_ALERT_COMMAND: true -ENABLE_CUSTOM_LOOKUP: true +ENABLE_ALERT_COMMAND: true +SHOW_ALERT_COMMAND_IN_MENU: false +SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: false ENABLE_7DAY_FORECAST: true ENABLE_5DAY_FORECAST: true ENABLE_HOURLY_WEATHER: true @@ -235,11 +248,13 @@ Description - DUTYCYCLE: false: If true, limits itself to 10% Dutycycle -- NWS_OFFICE: NWS_GRID_X: NWS_GRID_Y: #settings for the weather forecast api calls, see below to learn how to set up. +- ALERT_LAT: "34.0522" ALERT_LON: "-118.2433" # Location settings for alerts and forecast, put in the latitude and +longitude of the area you want coverage for. Make sure you only go up to 4 places past the decimal point on each. -- ALERT_LAT: "34.0522" ALERT_LON: "-118.2433" #settings for alerts, put in the latitude, and longitude of the area you -want alerts for. Make sure you only go up to 4 places past the decimal point on each. +- NWS_OFFICE: NWS_GRID_X: NWS_GRID_Y: #Can be left blank. These settings are used for manual entry of the weather +forecast api parameters. May be useful if you want your forecast generation for a different area than your alerts or if +the automatic configuration fails. See below for more info. - ALERT_CHECK_INTERVAL: # Time in seconds. How often the alert API is called. NWS does not publish allowable limits. @@ -247,7 +262,7 @@ From what I have gathered, they allow up to once a minute for alert checking. Yo - ALERT_INCLUDE_DESCRIPTION: #Set to false to exclude description from alerts. Descriptions will include alot of detail -such as every county, town, and area affected. You can expect about 4 or 5 messages when description is set to "true" vs +such as every county, town, and area affected. You can expect about 4 to 8 messages when description is set to "true" vs a single message when set to false. @@ -259,21 +274,24 @@ experimental. Hoping this may help with dropped 1st part of reply's, by giving t feel free to experiment with different values. -- MESSAGE_DELAY: Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order. +- MESSAGE_DELAY: # Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order. -- ENABLE_FULL_ALERT_COMMAND: #set to false to disable the "alert" command. Can produce up to 8 messages, may want to -disable on a high traffic mesh. +- ENABLE_ALERT_COMMAND: # Set to false to disable the alert request command, automatic alerts will not be affected. -- ENABLE_CUSTOM_LOOKUP: # Enable/disable custom lat/lon lookup via message. More info below. +- SHOW_ALERT_COMMAND_IN_MENU: # When false, hides the command from the menu but keeps it enabled, if enabled. + + +- SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: # Set to false to hide the custom lookup command from the menu. Command is always +accessible. - ENABLE_7DAY_FORECAST: ENABLE_5DAY_FORECAST: ENABLE_HOURLY_WEATHER: # These calls produce 2 to 4 messages each. If you are on a high-traffic mesh, you may want to disable these. -- FULL_MENU: true # When true, includes all weather commands. When false, shows only forecast options that return a +- FULL_MENU: # When true, includes all weather commands. When false, shows only forecast options that return a single message. @@ -303,7 +321,33 @@ unique the better. This is what NWS uses instead of an API key. Gives you the opportunity to fix the issue and stop getting throttled. -## How to get your NWS_OFFICE, NWS_GRID_X, and NWS_GRID_Y + +## Closing the program + +Press "Ctrl + c" once to tell the program to close. If Node shutdown is enabled in the settings.yaml The program will +command the node to shutdown and give it time to do so. + +Pressing "Ctrl + c" twice will force a hard exit of the program. + + +## Using the "Loc" custom location lookup command. +The loc command allows you to get a forecast for an area that is not the bots primary location. Input the locations +latitude and longitude along with the forecast type you want. + +Full command example: "loc 39.0453/-98.2077 hourly" + +Structure: loc {Latitude/longitude Command} command can be any of the regular commands like wind, 2day, 7day etc. +To ensure compatibility of your coordinates, only use up to 4 digits past the decimal point like in the example. + + +## Advance setup: How to get your NWS_OFFICE, NWS_GRID_X, and NWS_GRID_Y + + +Note: As of the latest update, these values are automatically set based on the ALERT_LAT and ALERT_LON coordinates. +Leave the grid parameters blank to enable automatic configuration. Only enter grid coordinates if you want to override +the automatic settings. + + To get your NWS office and grid coordinates: 1. Go to (https://weather.gov) 2. Enter your address @@ -327,24 +371,6 @@ For the alert settings in the settings.yaml file, enter your gps coordinates or earlier in this process. Use no more than four digits after the decimal point. -## Closing the program - -Press "Ctrl + c" once to tell the program to close. If Node shutdown is enabled in the settings.yaml The program will -command the node to shutdown and give it time to do so. - -Pressing "Ctrl + c" twice will force a hard exit of the program. - - -## Using the "Loc" custom location lookup command. -The loc command allows you to get a forecast for an area that is not the bots primary location. Input the locations -latitude and longitude along with the forecast type you want. - -Full command example: "loc 39.0453/-98.2077 hourly" - -Structure: loc {Latitude/longitude Command} command can be any of the regular commands like wind, 2day, 7day etc. -To ensure compatibility of your coordinates, only use up to 4 digits past the decimal point like in the example. - - ## API Handling details To prevent excessive api calls, the bot will check if it currently has the data being requested and if it is @@ -388,6 +414,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file This project is neither endorsed by nor supported by Meshtastic. Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various - -licenses, see GitHub for details. No warranty is provided - use at your own risk. - +licenses, see GitHub for details. No warranty is provided - use at your own risk. \ No newline at end of file diff --git a/img/Automaticgrid.png b/img/Automaticgrid.png new file mode 100644 index 0000000..c2b1a6e Binary files /dev/null and b/img/Automaticgrid.png differ diff --git a/img/Newfeatures.png b/img/Newfeatures.png deleted file mode 100644 index 0ce0aa5..0000000 Binary files a/img/Newfeatures.png and /dev/null differ diff --git a/img/newfeatures.png b/img/newfeatures0.png similarity index 100% rename from img/newfeatures.png rename to img/newfeatures0.png diff --git a/meshbot.py b/meshbot.py index 1e99079..1fbbdf8 100644 --- a/meshbot.py +++ b/meshbot.py @@ -34,6 +34,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +print(r""" +-----------------Welcome to Meshbot Weather------------------ +""") @@ -59,6 +62,89 @@ except ImportError: import serial.tools.list_ports import requests + + +def infer_nws_grid_from_coords(settings, logger=None): + """ + Fill in NWS_OFFICE, NWS_GRID_X, NWS_GRID_Y using ALERT_LAT and ALERT_LON. + Only runs if any NWS_* value is missing or empty. Updates settings in-place. + Returns True if values were inferred and updated; otherwise False. + """ + # If already complete, nothing to do + have_office = bool(str(settings.get("NWS_OFFICE", "")).strip()) + have_x = bool(str(settings.get("NWS_GRID_X", "")).strip()) + have_y = bool(str(settings.get("NWS_GRID_Y", "")).strip()) + if have_office and have_x and have_y: + office = str(settings.get("NWS_OFFICE", "")).strip() + grid_x = str(settings.get("NWS_GRID_X", "")).strip() + grid_y = str(settings.get("NWS_GRID_Y", "")).strip() + msg = f"Using NWS grid from settings.yaml: office={office}, grid=({grid_x}, {grid_y})" + logger_obj = globals().get("logger") + if logger_obj: + logger_obj.info(msg) + else: + print(msg) + return False + + # Need lat/lon for inference + lat_raw = settings.get("ALERT_LAT") + lon_raw = settings.get("ALERT_LON") + + + if not lat_raw or not lon_raw: + if logger: + logger.warning("Cannot determine NWS grid: ALERT_LAT/ALERT_LON not set.") + return False + + # Prepare request + try: + lat = float(lat_raw) + lon = float(lon_raw) + except (TypeError, ValueError): + if logger: + logger.warning("Cannot infer NWS grid: ALERT_LAT/ALERT_LON are not valid numbers.") + return False + + lat_s = f"{lat:.4f}" + lon_s = f"{lon:.4f}" + url = f"https://api.weather.gov/points/{lat_s},{lon_s}" + + user_agent_app = str(settings.get("USER_AGENT_APP", "meshbot-weather")) + user_agent_email = str(settings.get("USER_AGENT_EMAIL", "contact@example.com")) + headers = { + "Accept": "application/geo+json", + "User-Agent": f"({user_agent_app}, {user_agent_email})", + } + + try: + resp = requests.get(url, headers=headers, timeout=10) + resp.raise_for_status() + data = resp.json() + props = data.get("properties", {}) or {} + office = props.get("gridId") + grid_x = props.get("gridX") + grid_y = props.get("gridY") + + if not office or grid_x is None or grid_y is None: + if logger: + logger.warning("NWS Points API did not return grid info; leaving NWS_* unchanged.") + return False + + # Update as strings to match settings.yaml style + settings["NWS_OFFICE"] = str(office) + settings["NWS_GRID_X"] = str(grid_x) + settings["NWS_GRID_Y"] = str(grid_y) + + if logger: + logger.info(f"NWS grid auto-config: office={office}, x={grid_x}, y={grid_y}") + return True + + except requests.RequestException as e: + if logger: + logger.warning(f"Failed to fetch NWS grid from Points API: {e}") + return False + + from modules.temperature_24hour import Temperature24HourFetcher from modules.forecast_2day import Forecast2DayFetcher from modules.hourly_weather import EmojiWeatherFetcher @@ -107,11 +193,22 @@ alerts = None with open("settings.yaml", "r") as file: settings = yaml.safe_load(file) +ALERT_LAT = settings.get("ALERT_LAT") +ALERT_LON = settings.get("ALERT_LON") + + +logger.info(f"ALERT_LAT:{ALERT_LAT} ALERT_LON:{ALERT_LON}") + + +infer_nws_grid_from_coords(settings, logger=logger if 'logger' in globals() else None) + MYNODES = settings.get("MYNODES") DM_MODE = settings.get("DM_MODE") FIREWALL = settings.get("FIREWALL") DUTYCYCLE = settings.get("DUTYCYCLE") + + NWS_OFFICE = settings.get("NWS_OFFICE", "HNX") NWS_GRID_X = settings.get("NWS_GRID_X", "67") NWS_GRID_Y = settings.get("NWS_GRID_Y", "80") @@ -120,9 +217,14 @@ USER_AGENT_APP = settings.get("USER_AGENT_APP", "myweatherapp") USER_AGENT_EMAIL = settings.get("USER_AGENT_EMAIL", "contact@example.com") USER_AGENT = f"({USER_AGENT_APP}, {USER_AGENT_EMAIL})" -logger.info(f"DUTYCYCLE: {DUTYCYCLE}") -logger.info(f"DM_MODE: {DM_MODE}") -logger.info(f"FIREWALL: {FIREWALL}") + + +#logger.info(f"DUTYCYCLE: {DUTYCYCLE}") +#logger.info(f"DM_MODE: {DM_MODE}") +#logger.info(f"FIREWALL: {FIREWALL}") +#logger.info(f"MYNODES: {MYNODES}") + + transmission_count = 0 cooldown = False @@ -336,10 +438,20 @@ def message_listener(packet, interface): "rain - 24h precipitation\n" \ "temp - 24h temperature\n" # Add alert command if enabled - if settings.get('ENABLE_FULL_ALERT_COMMAND', True): + if settings.get("ENABLE_ALERT_COMMAND", True) and settings.get( + "SHOW_ALERT_COMMAND_IN_MENU", True): menu_text_2 += "alert - show active alerts\n" - if settings.get('ENABLE_CUSTOM_LOOKUP', False): + if settings.get('SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU', True): menu_text_2 += "loc lat/lon - custom location lookup\n" + #check if both show_alert and loc command are disabled + if not settings.get('SHOW_ALERT_COMMAND_IN_MENU', True) and not settings.get( + 'SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU', + False): + combined_menu = f"{menu_text_1}\n{menu_text_2}".strip() + # If both commands are disabled send menu without using split_message + interface.sendText(combined_menu,wantAck=True, destinationId=sender_id) + return + if settings.get('FULL_MENU', True): combined_menu = menu_text_1 + "\n" + menu_text_2 messages = split_message(combined_menu, message_type="Menu") @@ -350,10 +462,10 @@ def message_listener(packet, interface): "4day - 4 day forecast\n" \ "temp - 24h temperature\n" \ "rain - 24h precipitation" - if settings.get('ENABLE_FULL_ALERT_COMMAND', True): + if settings.get('ENABLE_ALERT_COMMAND', True): simple_menu += "\nalert - show active alerts" if settings.get('ENABLE_CUSTOM_LOOKUP', False): - simple_menu += "loc lat/lon - custom location lookup" + simple_menu += "\nloc lat/lon - custom location lookup" messages = split_message(simple_menu, message_type="Menu") send_message_sequence(messages, message_type="Menu") elif "loc" in message: @@ -447,7 +559,7 @@ def message_listener(packet, interface): if alerts: if not alerts.broadcast_full_alert(sender_id): time.sleep(first_message_delay) - if not settings.get('ENABLE_FULL_ALERT_COMMAND', True): + if not settings.get('ENABLE_ALERT_COMMAND', True): messages = split_message( "The full-alert command is disabled in settings.", message_type="Alert" ) @@ -477,7 +589,7 @@ def message_listener(packet, interface): def signal_handler(sig, frame): """Perform a graceful shutdown when CTRL+C is pressed""" global interface - logger.info("\nInitiating shutdown...") + logger.info("\nClosing program. Please wait...") try: if interface is not None: if settings.get('SHUTDOWN_NODE_ON_EXIT', False): @@ -492,7 +604,7 @@ def signal_handler(sig, frame): logger.error(f"Error sending shutdown command: {e}") else: - logger.info("Node shutdown disabled in settings, skipping shutdown command") + logger.info("Node shutdown disabled in settings, skipping sending power off command.") logger.info("Closing Meshtastic interface...") interface.close() @@ -536,7 +648,7 @@ def main(): logger.info("No serial ports found.") exit(0) - logger.info(f"Press CTRL-C to terminate the program") + logger.info(f"Press CTRL-C to close the program") # Create interface if args.host: @@ -657,4 +769,4 @@ def get_weather_alert_status(): if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/modules/weather_alert_monitor.py b/modules/weather_alert_monitor.py index 535d4d7..ef39c3f 100644 --- a/modules/weather_alert_monitor.py +++ b/modules/weather_alert_monitor.py @@ -81,7 +81,7 @@ class WeatherAlerts: def broadcast_full_alert(self, destination_id): """Broadcast the full alert information including description.""" # Check if full-alert command is enabled - if not self.settings.get('ENABLE_FULL_ALERT_COMMAND', True): + if not self.settings.get('ENABLE_ALERT_COMMAND', True): return False # Do nothing if full-alert command is disabled # Check if there's a current alert diff --git a/settings.yaml b/settings.yaml index 71fa442..dd268c7 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1,21 +1,22 @@ MYNODES: - "1234567890" #these are examples, fill in with your node numbers if needed, you can add lines as needed. - "1234567890" -FIREWALL: false # If true, only responds to node ids listed under "MYNODES:" +FIREWALL: false # If true, only responds to node ids listed under "MYNODES" DM_MODE: true # If true, bot responds to direct messages only. Recommend not changing this DUTYCYCLE: false # If true, will limit to 10% duty cycle -NWS_OFFICE: "BGM" # Location settings for the weather forecast api calls, see readme for details -NWS_GRID_X: "84" -NWS_GRID_Y: "89" -ALERT_LAT: "37.7654" # Location settings for alerts, use your coordinates. No more than 4 digits past the decimal point +ALERT_LAT: "37.7654" # Primary location settings for alerts and forecast. No more than 4 digits past the decimal point ALERT_LON: "-100.0151" +NWS_OFFICE: "" #Advance setup options, leave blank unless needed. See readme for details. +NWS_GRID_X: "" +NWS_GRID_Y: "" ALERT_CHECK_INTERVAL: 300 # Time in seconds between alert checks (default: 300 = 5 minutes) ALERT_INCLUDE_DESCRIPTION: false # Set to false to exclude the full description from automatically issued alerts ALERT_CHANNEL_INDEX: 0 # Channel index for weather alerts, default is 0 (first channel) FIRST_MESSAGE_DELAY: 0 # Delay in seconds between receiving a request and sending the first message back. MESSAGE_DELAY: 15 # Delay in seconds between subsequent messages of a multi-message response -ENABLE_FULL_ALERT_COMMAND: true # Set to false to disable the alert request command -ENABLE_CUSTOM_LOOKUP: true # Enable/disable custom lat/lon lookup via message +ENABLE_ALERT_COMMAND: true # Set to false to disable the alert request command, automatic alerts will not be affected. +SHOW_ALERT_COMMAND_IN_MENU: false # When false, hides the command from the menu but keeps it enabled, if enabled. +SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: true # When false, hides the command from the menu, but it is always enabled ENABLE_7DAY_FORECAST: true # Set to false to disable 7-day forecast module ENABLE_5DAY_FORECAST: true # Set to false to disable 5-day forecast module ENABLE_HOURLY_WEATHER: true # Set to false to disable hourly weather module