Compare commits

...

84 Commits
RC7 ... 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
SpudGunMan
9de72a26d0 bugfix
issue mentioned https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 15:48:23 -07:00
SpudGunMan
cd8a5bafcf cleanup 2024-08-07 12:38:34 -07:00
SpudGunMan
8a7b858edb enhanceRequestLocation 2024-08-07 12:35:01 -07:00
SpudGunMan
ab48622d23 cleanup 2024-08-07 12:05:17 -07:00
SpudGunMan
6eeba2fdbe space 2024-08-07 12:03:16 -07:00
SpudGunMan
b26d0d9f9d fixes 2024-08-07 12:02:32 -07:00
SpudGunMan
cda29f7b16 enhance file log 2024-08-07 11:55:54 -07:00
SpudGunMan
aaca4b5cb4 Update system.py 2024-08-07 11:52:29 -07:00
SpudGunMan
55460ee730 typo 2024-08-06 15:08:25 -07:00
SpudGunMan
94b0102205 enhance 2024-08-06 15:07:35 -07:00
SpudGunMan
dcd1c4235c logMessages 2 Disk 2024-08-06 14:45:39 -07:00
Kelly
4549e6786f Merge pull request #30 from SpudGunMan/cleanup
Cleanup
2024-08-06 14:35:56 -07:00
Kelly
2e7685e1ad Merge pull request #29 from SpudGunMan/main
frescoed
2024-08-06 14:33:08 -07:00
Kelly
4708557bb3 Merge pull request #28 from SpudGunMan/logging
Logging Enhancement and colors
2024-08-06 14:31:10 -07:00
SpudGunMan
2467b2f984 typo 2024-08-06 14:26:17 -07:00
SpudGunMan
fdd94b95b0 Update mesh_bot.py 2024-08-06 14:24:21 -07:00
SpudGunMan
dd3cc524ff enhance 2024-08-06 13:49:51 -07:00
SpudGunMan
0b71ec18a9 uwcolors
i know of corruption at uw
2024-08-06 13:38:45 -07:00
SpudGunMan
2e11d5a4fc Update log.py 2024-08-06 13:27:42 -07:00
SpudGunMan
5cc46fed8f colog
colors and logging
2024-08-06 13:04:05 -07:00
SpudGunMan
191837f1a6 Update mesh_bot.py 2024-08-05 23:30:24 -07:00
SpudGunMan
890843e394 Update system.py 2024-08-05 23:30:11 -07:00
SpudGunMan
85585db723 enhance 2024-08-05 23:20:44 -07:00
SpudGunMan
1719767a47 Update mesh_bot.py 2024-08-05 22:28:38 -07:00
16 changed files with 740 additions and 216 deletions

View File

@@ -8,9 +8,11 @@ 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`.
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages.
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.
The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
@@ -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

@@ -39,6 +39,18 @@ StoreLimit = 3
zuluTime = True
# wait time for URL requests
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
@@ -52,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

@@ -5,11 +5,12 @@
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
from modules.settings import *
from modules.log import *
from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
#Auto response to messages
bot_response = ""
if "ping" in message.lower():
#Check if the user added @foo to the message
if "@" in message:
@@ -22,11 +23,6 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓PONG, " + hop
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "pong" in message.lower():
bot_response = "🏓PING!!"
elif "motd" in message.lower():
@@ -57,7 +53,7 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "sun" in message.lower():
location = get_node_location(message_from_id, deviceID)
location = get_node_location(message_from_id, deviceID, channel_number)
bot_response = get_sun(str(location[0]),str(location[1]))
elif "hfcond" in message.lower():
bot_response = hf_band_conditions()
@@ -75,32 +71,38 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
elif "whereami" in message.lower():
location = get_node_location(message_from_id, deviceID)
location = get_node_location(message_from_id, deviceID, channel_number)
where = where_am_i(str(location[0]),str(location[1]))
bot_response = where
elif "tide" in message.lower():
location = get_node_location(message_from_id, deviceID)
location = get_node_location(message_from_id, deviceID, channel_number)
tide = get_tide(str(location[0]),str(location[1]))
bot_response = tide
elif "moon" in message.lower():
location = get_node_location(message_from_id, deviceID)
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():
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)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxa" in message.lower():
location = get_node_location(message_from_id, deviceID)
weatherAlert = getWeatherAlerts(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxc" in message.lower():
location = get_node_location(message_from_id, deviceID)
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)
weather = get_weather(str(location[0]),str(location[1]))
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()
@@ -108,44 +110,61 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
bot_response = bbs_list_messages()
elif "bbspost" in message.lower():
# Check if the user added a subject to the message
if "$" in message:
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
print(f"{log_timestamp()} System: BBS Post: {subject} Body: {body}")
logger.info(f"System: BBS Post: {subject} Body: {body}")
bot_response = bbs_post_message(subject,body,message_from_id)
else:
elif not "example:" in message:
bot_response = "example: bbspost $subject #message"
# Check if the user added a node number to the message
elif "@" in message:
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"
else:
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
bot_response = "example: bbspost @nodeNumber/ShortName #message"
elif not "example:" in 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
if "#" in message:
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_read_message(messageID)
else:
bot_response = "Please add a message number ex: bbsread #14"
elif not "example:" in message:
bot_response = "Please add a message number example: bbsread #14"
elif "bbsdelete" in message.lower():
# Check if the user added a message number to the message
if "#" in message:
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsdelete #14"
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "Please add a message number ex: bbsdelete #14"
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
bot_response = "🏓Testing 1,2,3"
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
@@ -192,7 +211,7 @@ def onReceive(packet, interface):
if msg:
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
print(f"{log_timestamp()} System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
bbs_delete_dm(msg[0], msg[1])
send_message(message, channel_number, message_from_id, rxNode)
@@ -203,31 +222,29 @@ def onReceive(packet, interface):
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
try:
snr = packet['rxSnr']
rssi = packet['rxRssi']
except KeyError:
snr = 0
rssi = 0
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
channel_number = packet.get('channel', 0)
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet['hopsAway']
hop_away = packet.get('hopsAway', 0)
else:
# if the packet does not have a hop count try other methods
hop_away = 0
if packet.get('hopLimit'):
hop_limit = packet['hopLimit']
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet['hopStart']
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
@@ -245,26 +262,29 @@ def onReceive(packet, interface):
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# ignore help and welcome messages
print(f"{log_timestamp()} Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
# message is DM to us
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
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.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -272,7 +292,7 @@ def onReceive(packet, interface):
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -280,8 +300,8 @@ def onReceive(packet, interface):
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# add the message to the message history but limit
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -302,47 +322,54 @@ def onReceive(packet, interface):
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
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.replace('\n', '-nl-'))
else:
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
# 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.replace('\n', '-nl-'))
except KeyError as e:
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
async def start_rx():
print ("\nMeshtastic Autoresponder Bot CTL+C to exit\n")
if bbs_enabled:
print(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if solar_conditions_enabled:
print(f"System: Celestial Telemetry Enabled")
if location_enabled:
print(f"System: Location Telemetry Enabled")
if dad_jokes_enabled:
print(f"System: Dad Jokes Enabled!")
if store_forward_enabled:
print(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
print(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
print(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
print(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
msg = (f"{log_timestamp()} System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
msg = (f"{log_timestamp()} System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
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 bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if solar_conditions_enabled:
logger.debug(f"System: Celestial Telemetry Enabled")
if location_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:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
# here we go loopty loo
while True:

View File

@@ -2,7 +2,7 @@
# K7MHI Kelly Keeton 2024
import pickle # pip install pickle
from modules.settings import *
from modules.log import *
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
@@ -18,14 +18,14 @@ def load_bbsdb():
bbs_messages = pickle.load(f)
except:
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
print ("\nSystem: Creating new bbsdb.pkl")
logger.debug("\nSystem: Creating new bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
def save_bbsdb():
global bbs_messages
# save the bbs messages to the database file
print ("System: Saving bbsdb.pkl\n")
logger.debug("System: Saving bbsdb.pkl\n")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
@@ -64,7 +64,7 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
return "Msg #" + str(messageID) + " deleted."
else:
print (f"!!System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
logger.warning(f"System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
return "You are not authorized to delete this message."
else:
return "Please specify a message number to delete."
@@ -75,12 +75,12 @@ def bbs_post_message(subject, message, fromNode):
# Check the BAN list for naughty nodes and silently drop the message
if str(fromNode) in bbs_ban_list:
print (f"!!System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
print (f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
# save the bbsdb
save_bbsdb()
@@ -100,7 +100,7 @@ def bbs_read_message(messageID = 0):
def save_bbsdm():
global bbs_dm
# save the bbs messages to the database file
print ("System: Saving Updated BBS Direct Messages bbsdm.pkl")
logger.debug("System: Saving Updated BBS Direct Messages bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
@@ -112,7 +112,7 @@ def load_bbsdm():
bbs_dm = pickle.load(f)
except:
bbs_dm = [[1234567890, "Message", 1234567890]]
print ("\nSystem: Creating new bbsdm.pkl")
logger.debug("\nSystem: Creating new bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
@@ -120,7 +120,7 @@ def bbs_post_dm(toNode, message, fromNode):
global bbs_dm
# Check the BAN list for naughty nodes and silently drop the message
if str(fromNode) in bbs_ban_list:
print (f"!!System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
return "DM Posted for node " + str(toNode)
# append the message to the list

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 = ""

62
modules/log.py Normal file
View File

@@ -0,0 +1,62 @@
import logging
from datetime import datetime
from modules.settings import *
class CustomFormatter(logging.Formatter):
grey = '\x1b[38;21m'
white = '\x1b[38;5;231m'
blue = '\x1b[38;5;39m'
yellow = '\x1b[38;5;226m'
red = '\x1b[38;5;196m'
green = '\x1b[38;5;46m'
purple = '\x1b[38;5;129m'
bold_red = '\x1b[31;1m'
bold_white = '\x1b[37;1m'
reset = '\x1b[0m'
def __init__(self, fmt):
super().__init__()
self.fmt = fmt
self.FORMATS = {
logging.DEBUG: self.blue + self.fmt + self.reset,
logging.INFO: self.white + self.fmt + self.reset,
logging.WARNING: self.yellow + self.fmt + self.reset,
logging.ERROR: self.red + self.fmt + self.reset,
logging.CRITICAL: self.bold_red + self.fmt + self.reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# Create logger
logger = logging.getLogger("MeshBot System Logger")
logger.setLevel(logging.DEBUG)
logger.propagate = False
msgLogger = logging.getLogger("MeshBot Messages Logger")
msgLogger.setLevel(logging.INFO)
msgLogger.propagate = False
# Define format for logs
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
msgLogFormat = '%(asctime)s | %(message)s'
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
# Set level for stdout handler (logs DEBUG level and above)
stdout_handler.setLevel(logging.DEBUG)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Create file handler for logging to a file
today = datetime.now()
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(msgLogFormat))
# Add handlers to the logger
logger.addHandler(stdout_handler)
if log_messages_to_file:
msgLogger.addHandler(file_handler)

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
@@ -72,10 +74,16 @@ try:
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
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)
@@ -87,9 +95,9 @@ 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.")
print(f"System: Exiting...")
exit(1)

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

@@ -4,10 +4,9 @@
import meshtastic.serial_interface #pip install meshtastic
import meshtastic.tcp_interface
import meshtastic.ble_interface
from datetime import datetime
import time
import asyncio
from modules.settings import *
from modules.log import *
# Global Variables
trap_list = ("cmd","cmd?") # default trap list
@@ -30,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:
@@ -50,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",)
@@ -68,10 +78,10 @@ try:
elif interface1_type == 'ble':
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
else:
print(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
print(f"System: Critical Error script abort. Initalizing Interface1 {e}")
logger.critical(f"System: script abort. Initalizing Interface1 {e}")
exit()
# Interface2 Configuration
@@ -84,10 +94,10 @@ if interface2_enabled:
elif interface2_type == 'ble':
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
else:
print(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
print(f"System: Critical Error script abort. Initalizing Interface2 {e}")
logger.critical(f"System: script abort. Initalizing Interface2 {e}")
exit()
#Get the node number of the device, check if the device is connected
@@ -95,7 +105,7 @@ try:
myinfo = interface1.getMyNodeInfo()
myNodeNum1 = myinfo['num']
except Exception as e:
print(f"System: Critical Error script abort. {e}")
logger.critical(f"System: script abort. {e}")
exit()
if interface2_enabled:
@@ -103,17 +113,11 @@ if interface2_enabled:
myinfo2 = interface2.getMyNodeInfo()
myNodeNum2 = myinfo2['num']
except Exception as e:
print(f"System: Critical Error script abort. {e}")
logger.critical(f"System: script abort. {e}")
exit()
else:
myNodeNum2 = 777
def log_timestamp():
if zuluTime:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
return datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
def decimal_to_hex(decimal_number):
return f"!{decimal_number:08x}"
@@ -149,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
@@ -171,7 +188,7 @@ def get_node_list(nodeInt=1):
item = (node_name, last_heard, snr)
node_list1.append(item)
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return ERROR_FETCHING_DATA
if nodeInt == 2:
@@ -182,33 +199,46 @@ 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)
node_list2.append(item)
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return ERROR_FETCHING_DATA
node_list1.sort(key=lambda x: x[1], reverse=True)
#print (f"Node List: {node_list1[:5]}\n")
node_list2.sort(key=lambda x: x[1], reverse=True)
try:
#print (f"Node List: {node_list1[:5]}\n")
node_list1.sort(key=lambda x: x[1], reverse=True)
#print (f"Node List: {node_list1[:5]}\n")
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
# 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
def get_node_location(number, nodeInt=1):
def get_node_location(number, nodeInt=1, channel=0):
# Get the location of a node by its number from nodeDB on device
latitude = latitudeValue
longitude = longitudeValue
@@ -222,15 +252,26 @@ def get_node_location(number, nodeInt=1):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
except Exception as e:
print (f"{log_timestamp()} System: Error getting location data for {number}")
print (f"System: location data for {number} is {latitude},{longitude}")
logger.error(f"System: Error getting location data for {number}")
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
position = [latitude,longitude]
return position
else:
print (f"{log_timestamp()} System: No location data for {number}")
logger.warning(f"System: No location data for {number} using default location")
# request location data
try:
logger.debug(f"System: Requesting location data for {number}")
if nodeInt == 1:
interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
if nodeInt == 2:
interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
except Exception as e:
logger.error(f"System: Error requesting location data for {number}. Error: {e}")
return position
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return position
if nodeInt == 2:
if interface2.nodes:
@@ -241,21 +282,89 @@ def get_node_location(number, nodeInt=1):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
except Exception as e:
print (f"{log_timestamp()} System: Error getting location data for {number}")
print (f"System: location data for {number} is {latitude},{longitude}")
logger.error(f"System: Error getting location data for {number}")
logger.info(f"System: location data for {number} is {latitude},{longitude}")
position = [latitude,longitude]
return position
else:
print (f"{log_timestamp()} System: No location data for {number}")
logger.warning(f"System: No location data for {number}")
return position
else:
print (f"{log_timestamp()} System: No nodes found")
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 == "":
return
# if message over MESSAGE_CHUNK_SIZE characters, split it into multiple messages
if len(message) > MESSAGE_CHUNK_SIZE:
print (f"{log_timestamp()} System: Splitting Message, Message Length: {len(message)}")
logger.debug(f"System: Splitting Message, Message Length: {len(message)}")
# split the message into MESSAGE_CHUNK_SIZE 160 character chunks
message = message.replace('\n', ' NEWLINE ') # replace newlines with NEWLINE to keep them in split chunks
@@ -281,14 +390,15 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
for m in message_list:
if nodeid == 0:
#Send to channel
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Multi-Chunk Message: {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
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} Multi-Chunk Message: {m} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
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)
if nodeInt == 2:
@@ -296,14 +406,15 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0:
# Send to channel
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Message: {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
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} {message} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
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)
if nodeInt == 2:
@@ -337,32 +448,31 @@ def messageTrap(msg):
def exit_handler():
# Close the interface and save the BBS messages
print(f"\n{log_timestamp()} System: Closing Autoresponder\n")
logger.debug(f"\nSystem: Closing Autoresponder\n")
try:
interface1.close()
print(f"{log_timestamp()} System: Interface1 Closed")
logger.debug(f"System: Interface1 Closed")
if interface2_enabled:
interface2.close()
print(f"{log_timestamp()} System: Interface2 Closed")
logger.debug(f"System: Interface2 Closed")
except Exception as e:
print(f"{log_timestamp()} System: Error closing: {e}")
logger.error(f"System: closing: {e}")
if bbs_enabled:
save_bbsdb()
save_bbsdm()
print(f"{log_timestamp()} System: BBS Messages Saved")
print(f"{log_timestamp()} System: Exiting")
logger.debug(f"System: BBS Messages Saved")
logger.debug(f"System: Exiting")
asyncLoop.stop()
asyncLoop.close()
exit (0)
async def handleSignalWatcher():
global lastHamLibAlert, antiSpam, sigWatchBrodcastCh
# monitor rigctld for signal strength and frequency
while True:
msg = await signalWatcher()
if msg != ERROR_FETCHING_DATA and msg is not None:
print(f"{log_timestamp()} System: Detected Alert from Hamlib {msg}")
logger.debug(f"System: Detected Alert from Hamlib {msg}")
# check we are not spammig the channel limit messages to once per minute
if time.time() - lastHamLibAlert > 60:
@@ -375,14 +485,14 @@ async def handleSignalWatcher():
if interface2_enabled:
send_message(msg, int(ch), 0, 2)
else:
print(f"{log_timestamp()} System: antiSpam prevented Alert from Hamlib {msg}")
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
else:
if antiSpam and sigWatchBrodcastCh != publicChannel:
send_message(msg, int(sigWatchBrodcastCh), 0, 1)
if interface2_enabled:
send_message(msg, int(sigWatchBrodcastCh), 0, 2)
else:
print(f"{log_timestamp()} System: antiSpam prevented Alert from Hamlib {msg}")
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
await asyncio.sleep(1)
pass
@@ -398,7 +508,7 @@ async def retry_interface(nodeID=1):
try:
interface1.close()
except Exception as e:
print(f"{log_timestamp()} System: Error closing interface1: {e}")
logger.error(f"System: closing interface1: {e}")
if nodeID==2:
if interface2 is not None:
retry_int2 = True
@@ -406,15 +516,15 @@ async def retry_interface(nodeID=1):
try:
interface2.close()
except Exception as e:
print(f"{log_timestamp()} System: Error closing interface2: {e}")
logger.error(f"System: closing interface2: {e}")
print(f"{log_timestamp()} System: Retrying interface in 15 seconds")
logger.debug(f"System: Retrying interface in 15 seconds")
if max_retry_count1 == 0:
print(f"{log_timestamp()} System: Max retry count reached for interface1")
logger.critical(f"System: Max retry count reached for interface1")
exit_handler()
if max_retry_count2 == 0:
print(f"{log_timestamp()} System: Max retry count reached for interface2")
logger.critical(f"System: Max retry count reached for interface2")
exit_handler()
# wait 15 seconds before retrying
await asyncio.sleep(15)
@@ -423,32 +533,32 @@ async def retry_interface(nodeID=1):
try:
if nodeID==1 and retry_int1:
interface1 = None
print(f"{log_timestamp()} System: Retrying Interface1")
logger.debug(f"System: Retrying Interface1")
if interface1_type == 'serial':
interface1 = meshtastic.serial_interface.SerialInterface(port1)
elif interface1_type == 'tcp':
interface1 = meshtastic.tcp_interface.TCPInterface(hostname1)
elif interface1_type == 'ble':
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
print(f"{log_timestamp()} System: Interface1 Opened!")
logger.debug(f"System: Interface1 Opened!")
retry_int1 = False
except Exception as e:
print(f"{log_timestamp()} System: Error opening interface1 on: {e}")
logger.error(f"System: opening interface1 on: {e}")
try:
if nodeID==2 and retry_int2:
interface2 = None
print(f"{log_timestamp()} System: Retrying Interface2")
logger.debug(f"System: Retrying Interface2")
if interface2_type == 'serial':
interface2 = meshtastic.serial_interface.SerialInterface(port2)
elif interface2_type == 'tcp':
interface2 = meshtastic.tcp_interface.TCPInterface(hostname2)
elif interface2_type == 'ble':
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
print(f"{log_timestamp()} System: Interface2 Opened!")
logger.debug(f"System: Interface2 Opened!")
retry_int2 = False
except Exception as e:
print(f"{log_timestamp()} System: Error opening interface2: {e}")
logger.error(f"System: opening interface2: {e}")
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
@@ -468,24 +578,53 @@ 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)
#print(f"{log_timestamp()} System: watchdog running\r", end="")
#print(f"MeshBot System: watchdog running\r", end="")
if interface1 is not None and not retry_int1:
try:
with suppress_stdout():
interface1.localNode.getMetadata()
#if "device_state_version:" not in meta:
except Exception as e:
print(f"{log_timestamp()} System: Error communicating with interface1, trying to reconnect: {e}")
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)
except Exception as e:
print(f"{log_timestamp()} System: Error retrying interface1: {e}")
logger.error(f"System: retrying interface1: {e}")
if interface2_enabled:
if interface2 is not None and not retry_int2:
@@ -493,12 +632,33 @@ async def watchdog():
with suppress_stdout():
interface2.localNode.getMetadata()
except Exception as e:
print(f"{log_timestamp()} System: Error communicating with interface2, trying to reconnect: {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:
print(f"{log_timestamp()} System: Error retrying interface2: {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

@@ -5,7 +5,7 @@
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
from modules.settings import *
from modules.log import *
from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
@@ -22,11 +22,6 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓PONG, " + hop
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "pong" in message.lower():
bot_response = "🏓Ping!!"
elif "motd" in message.lower():
@@ -48,8 +43,16 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
bot_response = "🏓Testing 1,2,3"
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
@@ -131,7 +134,7 @@ def onReceive(packet, interface):
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# ignore help and welcome messages
print(f"{log_timestamp()} Got Own Welcome/Help header. Device:{rxNode} From: {get_name_from_number(message_from_id)}")
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
@@ -140,25 +143,29 @@ def onReceive(packet, interface):
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
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}")
else:
# message is on a channel
if messageTrap(message_string):
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# or respond to channel message on the channel itself
if channel_number == publicChannel:
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -166,8 +173,8 @@ def onReceive(packet, interface):
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# add the message to the message history but limit
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -188,30 +195,42 @@ def onReceive(packet, interface):
time.sleep(0.7)
if str(channel_number) in repeater_channels:
if rxNode == 1:
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
else:
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
# nothing to do for us
logger.info(f"Ignoring Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "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}")
except KeyError as e:
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
msg = (f"{log_timestamp()} System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
msg = (f"{log_timestamp()} System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
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:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
# here we go loopty loo
while True:

View File

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