forked from iarv/meshing-around
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dabd20a2e | ||
|
|
d8e5cb7893 | ||
|
|
28514adf00 | ||
|
|
bfa8aa0a86 | ||
|
|
9e205155a5 | ||
|
|
1e921dd5ea | ||
|
|
5c73e49610 | ||
|
|
91f11e4828 | ||
|
|
4a9c969dc0 | ||
|
|
88e960ae33 | ||
|
|
0217f4f2cc | ||
|
|
29fb8b0b40 | ||
|
|
773ee78fb2 | ||
|
|
d43e28d723 | ||
|
|
d063fdd81d | ||
|
|
f73cd5ec31 | ||
|
|
35df43b727 | ||
|
|
e17999a2d6 | ||
|
|
9f658fc060 | ||
|
|
27ece919d7 | ||
|
|
0e97953adf | ||
|
|
66d44c3a6d | ||
|
|
66ca1b4103 | ||
|
|
0b3040f7b7 | ||
|
|
066f7edfd9 | ||
|
|
72f049452b | ||
|
|
c1b493b7c7 | ||
|
|
67af1ba39e | ||
|
|
c48851719a | ||
|
|
cfbda17cfb | ||
|
|
be32fd4a17 | ||
|
|
98b9e0471c | ||
|
|
9efbbb4f20 | ||
|
|
7b8779fc48 | ||
|
|
07e6042e67 | ||
|
|
814303c521 | ||
|
|
2673b638bf | ||
|
|
92b7b7ae2a | ||
|
|
7d63c2dc11 | ||
|
|
514facacd5 | ||
|
|
89dc8791d0 | ||
|
|
700f65ce73 | ||
|
|
4f24701460 | ||
|
|
0514d51aea | ||
|
|
99a05c66ef | ||
|
|
e533e1472e | ||
|
|
ab00cb11bb | ||
|
|
932b98a634 | ||
|
|
b084b0f79e | ||
|
|
115d479020 | ||
|
|
1cb9a60bba | ||
|
|
14c304ca2d | ||
|
|
88d1ecc7ec | ||
|
|
7cabff0bc4 | ||
|
|
5e0ab39301 | ||
|
|
f6ff4e2d7d | ||
|
|
49c0f3b1c5 | ||
|
|
fbd38aa147 | ||
|
|
922956e981 | ||
|
|
ba1447d5f4 |
34
README.md
34
README.md
@@ -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`
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
62
mesh_bot.py
62
mesh_bot.py
@@ -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:
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
177
modules/wx_meteo.py
Normal 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -7,3 +7,7 @@ geopy
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
Reference in New Issue
Block a user