Compare commits

...

60 Commits
RC8 ... RC9

Author SHA1 Message Date
Kelly
4dabd20a2e Merge pull request #38 from SpudGunMan/sentry
Sentry Mode
2024-08-11 23:04:33 -07:00
SpudGunMan
d8e5cb7893 Update config.template 2024-08-11 23:03:48 -07:00
SpudGunMan
28514adf00 enhance 2024-08-11 23:02:19 -07:00
SpudGunMan
bfa8aa0a86 enhance
fix some issues raised https://github.com/SpudGunMan/meshing-around/issues/37 thankyou!
2024-08-10 12:37:22 -07:00
SpudGunMan
9e205155a5 Update system.py 2024-08-10 09:58:30 -07:00
SpudGunMan
1e921dd5ea geopy tired of maths 2024-08-10 01:48:12 -07:00
SpudGunMan
5c73e49610 Update mesh_bot.py 2024-08-10 00:25:01 -07:00
SpudGunMan
91f11e4828 enhance 2024-08-10 00:17:24 -07:00
SpudGunMan
4a9c969dc0 Update mesh_bot.py 2024-08-10 00:04:42 -07:00
SpudGunMan
88e960ae33 cant handle 🥔 2024-08-10 00:03:08 -07:00
Kelly
0217f4f2cc Merge pull request #35 from SpudGunMan/main
PullinMain
2024-08-09 23:45:56 -07:00
SpudGunMan
29fb8b0b40 Update mesh_bot.py 2024-08-09 23:22:57 -07:00
SpudGunMan
773ee78fb2 Update mesh_bot.py 2024-08-09 23:22:24 -07:00
SpudGunMan
d43e28d723 Update README.md 2024-08-09 23:21:50 -07:00
SpudGunMan
d063fdd81d Update README.md 2024-08-09 23:21:13 -07:00
SpudGunMan
f73cd5ec31 cleanup 2024-08-09 23:19:30 -07:00
SpudGunMan
35df43b727 bbspost by shortname
this is very basic for now
2024-08-09 23:09:21 -07:00
SpudGunMan
e17999a2d6 numpy.. 2024-08-08 23:04:51 -07:00
SpudGunMan
9f658fc060 Update wx_meteo.py 2024-08-08 22:52:04 -07:00
SpudGunMan
27ece919d7 Update system.py 2024-08-08 12:41:29 -07:00
SpudGunMan
0e97953adf Update system.py 2024-08-08 12:32:17 -07:00
SpudGunMan
66d44c3a6d ignorelist 2024-08-08 12:21:55 -07:00
Kelly
66ca1b4103 Merge pull request #34 from SpudGunMan/main
fix depends
2024-08-08 11:54:08 -07:00
SpudGunMan
0b3040f7b7 fix depends 2024-08-08 11:53:27 -07:00
SpudGunMan
066f7edfd9 Revert "fix depends"
This reverts commit 72f049452b.
2024-08-08 11:52:13 -07:00
SpudGunMan
72f049452b fix depends 2024-08-08 11:51:50 -07:00
SpudGunMan
c1b493b7c7 Update system.py 2024-08-08 03:35:43 -07:00
SpudGunMan
67af1ba39e sentry notification 2024-08-08 02:57:52 -07:00
SpudGunMan
c48851719a Update system.py 2024-08-08 02:53:13 -07:00
SpudGunMan
cfbda17cfb Update system.py 2024-08-08 02:06:52 -07:00
SpudGunMan
be32fd4a17 Update system.py 2024-08-08 02:04:21 -07:00
SpudGunMan
98b9e0471c Update README.md 2024-08-08 02:00:39 -07:00
SpudGunMan
9efbbb4f20 Update system.py 2024-08-08 02:00:24 -07:00
SpudGunMan
7b8779fc48 Update system.py 2024-08-08 01:58:39 -07:00
SpudGunMan
07e6042e67 Update system.py 2024-08-08 01:55:55 -07:00
SpudGunMan
814303c521 fix 2024-08-08 01:47:00 -07:00
SpudGunMan
2673b638bf newidea 2024-08-08 01:42:27 -07:00
SpudGunMan
92b7b7ae2a cleanup messageLog 2024-08-08 00:11:12 -07:00
SpudGunMan
7d63c2dc11 tidy output for not printing \n 2024-08-07 22:56:36 -07:00
SpudGunMan
514facacd5 cleanup 2024-08-07 22:46:30 -07:00
SpudGunMan
89dc8791d0 fix debug 2024-08-07 21:55:36 -07:00
SpudGunMan
700f65ce73 Update system.py
resolution for https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 21:53:36 -07:00
SpudGunMan
4f24701460 Update locationdata.py 2024-08-07 21:26:54 -07:00
SpudGunMan
0514d51aea cleanup 2024-08-07 21:24:35 -07:00
Kelly
99a05c66ef Merge pull request #33 from SpudGunMan/worldwx
World Weather
2024-08-07 20:08:23 -07:00
SpudGunMan
e533e1472e fix 2024-08-07 20:06:19 -07:00
SpudGunMan
ab00cb11bb Update system.py 2024-08-07 20:05:27 -07:00
SpudGunMan
932b98a634 debug 2024-08-07 20:04:01 -07:00
SpudGunMan
b084b0f79e Update mesh_bot.py 2024-08-07 19:59:34 -07:00
Kelly
115d479020 Merge branch 'main' into worldwx 2024-08-07 19:53:37 -07:00
SpudGunMan
1cb9a60bba Update system.py 2024-08-07 19:42:48 -07:00
SpudGunMan
14c304ca2d Update system.py 2024-08-07 19:40:41 -07:00
SpudGunMan
88d1ecc7ec Update system.py 2024-08-07 19:40:20 -07:00
SpudGunMan
7cabff0bc4 Update wx_meteo.py 2024-08-07 19:27:47 -07:00
SpudGunMan
5e0ab39301 requirements update 2024-08-07 19:27:29 -07:00
SpudGunMan
f6ff4e2d7d short-strings 2024-08-07 19:20:24 -07:00
SpudGunMan
49c0f3b1c5 Update wx_meteo.py 2024-08-07 19:10:03 -07:00
SpudGunMan
fbd38aa147 Update README.md
addressing issue https://github.com/SpudGunMan/meshing-around/issues/23
2024-08-07 18:52:42 -07:00
SpudGunMan
922956e981 Update wx_meteo.py 2024-08-07 18:12:58 -07:00
SpudGunMan
ba1447d5f4 inital 2024-08-07 18:04:22 -07:00
14 changed files with 484 additions and 61 deletions

View File

@@ -8,7 +8,9 @@ The feature-rich bot requires the internet for full functionality. These respond
Along with network testing, this bot has a lot of other features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM.
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShportName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
The bot will report on anyone who is getting close to the device if in a remote location.
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
@@ -24,12 +26,12 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
- `bbshelp` returns the following
- `bbslist` list the messages by ID and subject
- `bbsread` read a message example use: `bbsread #1`
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message`
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message or bbspost @nodeShportName #message`
- `bbsdelete` delete a message example use: `bbsdelete #4`
- 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 +82,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.
```
@@ -90,6 +100,17 @@ enabled = False
DadJokes = False
StoreForward = False
```
Sentry Bot detects anyone comeing close to the bot-node
```
# detect anyone close to the bot
SentryEnabled = True
# holdoff time multiplied by minutes(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
```
The BBS has admin and block lists; see the [config.template](config.template)
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
@@ -132,6 +153,13 @@ pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install geopy
```
The following is needed for open-meteo use
```
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`

View File

@@ -41,6 +41,16 @@ zuluTime = True
URL_TIMEOUT = 10
# logging to file of the non Bot messages
LogMessagesToFile = False
# detect anyone close to the bot
SentryEnabled = True
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by minutes(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
[bbs]
enabled = True
@@ -54,10 +64,14 @@ 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
# Default to metric units rather than imperial
useMetric = False
# solar module
[solar]

View File

@@ -8,7 +8,7 @@ After=network.target
[Service]
WorkingDirectory=/dir/
ExecStart=/usr/bin/python /dir/launch.sh mesh
ExecStart=/usr/bin/bash /dir/launch.sh mesh
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

View File

@@ -8,7 +8,7 @@ After=network.target
[Service]
WorkingDirectory=/dir/
ExecStart=/usr/bin/python /dir/launch.sh pong
ExecStart=/usr/bin/bash /dir/launch.sh pong
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

View File

@@ -14,9 +14,9 @@ fi
# launch the application
if [ "$1" == "pong" ]; then
python pong_bot.py
python3 pong_bot.py
elif [ "$1" == "mesh" ]; then
python mesh_bot.py
python3 mesh_bot.py
else
printf "\nPlease provide a bot to launch (pong/mesh)"
fi

View File

@@ -82,21 +82,27 @@ 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 "wxc" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
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]))
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() or "wx" in message.lower():
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in message.lower() and not use_metric:
logger.debug(f"System: Bot Returning Open-Meteo API for weather imperial")
weather = get_wx_meteo(str(location[0]),str(location[1]))
elif use_meteo_wxApi:
logger.debug(f"System: Bot Returning Open-Meteo API for weather metric")
weather = get_wx_meteo(str(location[0]),str(location[1]),1)
elif not use_meteo_wxApi and "wxc" in message.lower() or use_metric:
logger.debug(f"System: Bot Returning NOAA API for weather metric")
weather = get_weather(str(location[0]),str(location[1]),1)
else:
logger.debug(f"System: Bot Returning NOAA API for weather imperial")
weather = get_weather(str(location[0]),str(location[1]))
bot_response = weather
elif "joke" in message.lower():
bot_response = tell_joke()
@@ -118,13 +124,22 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
# if toNode is a string look for short name and convert to number
if toNode.isalpha() or not toNode.isnumeric():
toNode = get_num_from_short_name(toNode, deviceID)
if toNode == 0:
bot_response = "Node not found " + message.split("@")[1].split("#")[0]
return bot_response
else:
logger.debug(f"System: bbspost, name lookup found: {toNode}")
if "#" in message:
body = message.split("#")[1]
bot_response = bbs_post_dm(toNode, body, message_from_id)
else:
bot_response = "example: bbspost @nodeNumber #message"
bot_response = "example: bbspost @nodeNumber/ShortName #message"
elif not "example:" in message:
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
bot_response = "example: bbspost $subject #message, or bbspost @node #message"
elif "bbsread" in message.lower():
# Check if the user added a message number to the message
@@ -263,7 +278,7 @@ def onReceive(packet, interface):
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
@@ -312,12 +327,12 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
# nothing to do for us
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
@@ -339,9 +354,14 @@ 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 sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:

View File

@@ -7,7 +7,7 @@ import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.settings import *
from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
@@ -16,6 +16,7 @@ def where_am_i(lat=0, lon=0):
grid = mh.to_maiden(float(lat), float(lon))
if float(lat) == 0 and float(lon) == 0:
logger.error("Location: No GPS data, cant find where you are")
return NO_DATA_NOGPS
# initialize Nominatim API
@@ -41,6 +42,7 @@ def where_am_i(lat=0, lon=0):
def get_tide(lat=0, lon=0):
station_id = ""
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, cant find where you are for tide")
return NO_DATA_NOGPS
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
try:
@@ -48,14 +50,17 @@ def get_tide(lat=0, lon=0):
if station_data.ok:
station_json = station_data.json()
else:
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA
station_id = station_json['stationList'][0]['stationId']
except (requests.exceptions.RequestException, json.JSONDecodeError):
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
@@ -65,8 +70,10 @@ def get_tide(lat=0, lon=0):
try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok:
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA
# extract table class="table table-condensed"
@@ -97,7 +104,10 @@ def get_weather(lat=0, lon=0, unit=0):
if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS
# get weather data from NOAA units for metric
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
unit = 1
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
if unit == 1:
weather_url += "&unit=1"
@@ -105,14 +115,17 @@ def get_weather(lat=0, lon=0, unit=0):
try:
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
if not weather_data.ok:
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
table = soup.find('div', id="detailed-forecast-body")
if table is None:
logger.error("Location:Bad weather data from NOAA")
return ERROR_FETCHING_DATA
else:
# get rows
@@ -200,8 +213,10 @@ def getWeatherAlerts(lat=0, lon=0):
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.error("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
alerts = ""
@@ -233,6 +248,7 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, cant find where you are for weather alerts")
return NO_DATA_NOGPS
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
@@ -241,8 +257,10 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.error("Location:Error fetching weather alerts detailed from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts detailed from NOAA")
return ERROR_FETCHING_DATA
alerts = ""

View File

@@ -5,7 +5,7 @@
import socket
import asyncio
from modules.settings import *
from modules.log import *
def get_hamlib(msg="f"):
try:
@@ -13,7 +13,7 @@ def get_hamlib(msg="f"):
rigControlSocket.settimeout(2)
rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1])))
except Exception as e:
print(f"\nSystem: Error connecting to rigctld: {e}")
logger.error(f"RadioMon: Error connecting to rigctld: {e}")
return ERROR_FETCHING_DATA
try:
@@ -27,7 +27,7 @@ def get_hamlib(msg="f"):
data = data.replace(b'\n',b'')
return data.decode("utf-8").rstrip()
except Exception as e:
print(f"\nSystem: Error fetching data from rigctld: {e}")
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
return ERROR_FETCHING_DATA
def get_freq_common_name(freq):
@@ -140,6 +140,7 @@ async def signalWatcher():
signalStrength = int(get_sig_strength())
if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold:
message = f"Detected {get_freq_common_name(get_hamlib('f'))} active. S-Meter:{signalStrength}dBm"
logger.debug(f"RadioMon: {message}. Waiting for {signalHoldTime} seconds")
previousStrength = signalStrength
signalCycle = 0
await asyncio.sleep(signalHoldTime)

View File

@@ -64,6 +64,8 @@ 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
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
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
@@ -73,10 +75,15 @@ try:
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
store_forward_enabled = config['general'].getboolean('StoreForward', False)
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
sentry_enabled = config['general'].getboolean('SentryEnabled', True) # default True
secure_channel = config['general'].getint('SentryChannel', 2) # default 2
sentry_holdoff = config['general'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['general'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['general'].getint('SentryRadius', 100) # default 100 meters
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)
@@ -88,6 +95,7 @@ try:
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
except KeyError as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")

View File

@@ -7,7 +7,7 @@ import xml.dom.minidom
from datetime import datetime
import ephem # pip install pyephem
from datetime import timedelta
from modules.settings import *
from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond")
@@ -19,9 +19,11 @@ def hf_band_conditions():
solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
hf_cond = hf_cond[:-1] # remove the last newline
else:
hf_cond += ERROR_FETCHING_DATA
hf_cond = hf_cond[:-1] # remove the last newline
logger.error("Solar: Error fetching HF band conditions")
hf_cond = ERROR_FETCHING_DATA
return hf_cond
def solar_conditions():
@@ -39,7 +41,8 @@ def solar_conditions():
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
else:
solar_cond += ERROR_FETCHING_DATA
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond
def drap_xray_conditions():
@@ -53,7 +56,8 @@ def drap_xray_conditions():
if x_filter in line:
xray_flux = line.split(": ")[1]
else:
xray_flux += ERROR_FETCHING_DATA
logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux
def get_sun(lat=0, lon=0):

View File

@@ -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:
@@ -49,6 +56,10 @@ if dad_jokes_enabled:
trap_list = trap_list + ("joke",)
help_message = help_message + ", joke"
if sentry_enabled:
from math import sqrt
import geopy.distance # pip install geopy
# Store and Forward Configuration
if store_forward_enabled:
trap_list = trap_list + ("messages",)
@@ -142,6 +153,19 @@ def get_name_from_number(number, type='long', nodeInt=1):
name = str(decimal_to_hex(number)) # If name not found, use the ID as string
return name
return number
def get_num_from_short_name(short_name, nodeInt=1):
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}")
if nodeInt == 1:
for node in interface1.nodes.values():
if str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
if nodeInt == 2:
for node in interface2.nodes.values():
if str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
return 0
def get_node_list(nodeInt=1):
# Get a list of nodes on the device
@@ -175,8 +199,10 @@ def get_node_list(nodeInt=1):
node_name = get_name_from_number(node['num'], 'long', nodeInt)
snr = node.get('snr', 0)
# issue where lastHeard is not always present
# issue where lastHeard is not always present, also had issues with None
last_heard = node.get('lastHeard', 0)
if last_heard is None:
last_heard = 0
# make a list of nodes with last heard time and SNR
item = (node_name, last_heard, snr)
@@ -192,18 +218,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")
#print (f"Node List1: {node_list1[:5]}\n")
#print (f"Node List2: {node_list2[:5]}\n")
node_list = ERROR_FETCHING_DATA
# 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
@@ -261,6 +292,72 @@ def get_node_location(number, nodeInt=1, channel=0):
else:
logger.warning(f"System: No nodes found")
return position
def get_closest_nodes(nodeInt=1,returnCount=3):
node_list = []
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
# set radius around BOT position
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
# calculate distance to node and report
except Exception as e:
pass
# else:
# # request location data
# try:
# logger.debug(f"System: Requesting location data for {node['id']}")
# interface1.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=publicChannel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
if nodeInt == 2:
if interface2.nodes:
for node in interface2.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
# set radius around BOT position
distance = geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
# calculate distance to node and report
except Exception as e:
pass
#sort by distance closest to lattitudeValue, longitudeValue
node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
def send_message(message, ch, nodeid=0, nodeInt=1):
if message == "":
@@ -293,14 +390,14 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
for m in message_list:
if nodeid == 0:
#Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + f"{m}")
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + m.replace('\n', ' '))
if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch)
if nodeInt == 2:
interface2.sendText(text=m, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + f"{m}" + CustomFormatter.purple +\
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch, destinationId=nodeid)
@@ -309,14 +406,14 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0:
# Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + f"{message}")
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + message.replace('\n', ' '))
if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch)
if nodeInt == 2:
interface2.sendText(text=message, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + f"{message}" + CustomFormatter.purple +\
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch, destinationId=nodeid)
@@ -481,6 +578,13 @@ def suppress_stdout():
async def watchdog():
global retry_int1, retry_int2
if sentry_enabled:
sentry_loop = 0
lastSpotted = ""
enemySpotted = ""
sentry_loop2 = 0
lastSpotted2 = ""
enemySpotted2 = ""
# watchdog for connection to the interface
while True:
await asyncio.sleep(20)
@@ -494,6 +598,28 @@ async def watchdog():
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
retry_int1 = True
# Locate Closest Nodes and report them to a secure channel
if sentry_enabled:
try:
closest_nodes1 = get_closest_nodes(1)
if closest_nodes1 != ERROR_FETCHING_DATA:
if closest_nodes1[0]['id'] is not None:
enemySpotted = get_name_from_number(closest_nodes1[0]['id'], 'long', 1)
enemySpotted += ", " + get_name_from_number(closest_nodes1[0]['id'], 'short', 1)
enemySpotted += ", " + str(closest_nodes1[0]['id'])
enemySpotted += ", " + decimal_to_hex(closest_nodes1[0]['id'])
enemySpotted += f" at {closest_nodes1[0]['distance']}m"
except Exception as e:
pass
if sentry_loop >= sentry_holdoff and lastSpotted != enemySpotted:
logger.warning(f"System: {enemySpotted} is close to your location on Interface1")
send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 1)
sentry_loop = 0
lastSpotted = enemySpotted
else:
sentry_loop += 1
if retry_int1:
try:
await retry_interface(1)
@@ -508,10 +634,31 @@ async def watchdog():
except Exception as e:
logger.error(f"System: communicating with interface2, trying to reconnect: {e}")
retry_int2 = True
# Locate Closest Nodes and report them to a secure channel
if sentry_enabled:
try:
closest_nodes2 = get_closest_nodes(2)
if closest_nodes2 != ERROR_FETCHING_DATA:
if closest_nodes2[0]['id'] is not None:
enemySpotted2 = get_name_from_number(closest_nodes2[0]['id'], 'long', 2)
enemySpotted2 += ", " + get_name_from_number(closest_nodes2[0]['id'], 'short', 2)
enemySpotted2 += ", " + str(closest_nodes2[0]['id'])
enemySpotted2 += ", " + decimal_to_hex(closest_nodes2[0]['id'])
enemySpotted += f" at {closest_nodes1[0]['distance']}m"
except Exception as e:
pass
if sentry_loop2 >= sentry_holdoff and lastSpotted2 != enemySpotted2:
logger.warning(f"System: {enemySpotted2} is close to your location on Interface2")
send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 2)
sentry_loop2 = 0
lastSpotted2 = enemySpotted2
else:
sentry_loop2 += 1
if retry_int2:
try:
await retry_interface(2)
except Exception as e:
logger.error(f"System: retrying interface2: {e}")

177
modules/wx_meteo.py Normal file
View File

@@ -0,0 +1,177 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#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"
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
except Exception as e:
logger.error(f"Error fetching meteo weather data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
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()
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
# 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

View File

@@ -221,6 +221,8 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if sentry_enabled:
logger.debug(f"System: Sentry Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:

View File

@@ -7,3 +7,7 @@ geopy
maidenhead
beautifulsoup4
dadjokes
openmeteo_requests
retry_requests
numpy
geopy