mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d33cc270 | ||
|
|
a6ce9e9211 | ||
|
|
60bdabdd1b | ||
|
|
9c5c2080cf | ||
|
|
8f758229cb | ||
|
|
8ac9c53f1a | ||
|
|
98cbf5528c | ||
|
|
6296150677 | ||
|
|
13cb1e8df9 | ||
|
|
e26e876ccf | ||
|
|
550b50f74e | ||
|
|
ac5aa1a201 | ||
|
|
19700f54c5 | ||
|
|
7e5626cd30 | ||
|
|
c27b6ed8a1 | ||
|
|
717181bcd0 | ||
|
|
4d5916df29 | ||
|
|
93b7a1d613 | ||
|
|
35cc029984 | ||
|
|
589d44c152 | ||
|
|
06a14d875f | ||
|
|
454f823ad7 | ||
|
|
6974c4ef66 | ||
|
|
bd956dfebc | ||
|
|
4aaac5ba49 | ||
|
|
2ae792dd8d | ||
|
|
ca033f024e | ||
|
|
ad11f787de | ||
|
|
e3d1607c86 | ||
|
|
b68461cbc8 | ||
|
|
ddad35aa1e | ||
|
|
35f4aad6f8 | ||
|
|
f08f98e040 | ||
|
|
467376d9c7 | ||
|
|
1cbdc93632 | ||
|
|
2323015617 | ||
|
|
51de0dee8a | ||
|
|
b74c0ebd36 | ||
|
|
0a4c54a5a2 | ||
|
|
481809493c | ||
|
|
c3914e0423 | ||
|
|
ac40254bc4 | ||
|
|
b6540a1d20 | ||
|
|
87d29d123f | ||
|
|
0aa6f8cc07 | ||
|
|
e2bb480f5f | ||
|
|
920f951e47 | ||
|
|
215fe76f2a | ||
|
|
1740bbf666 | ||
|
|
f9370d47b4 | ||
|
|
91072cb47d | ||
|
|
c30be37f02 | ||
|
|
d51dadba04 | ||
|
|
99c404f479 | ||
|
|
659ee2959c | ||
|
|
1ac9f3b0d6 | ||
|
|
d0dc737863 | ||
|
|
e438c82a11 | ||
|
|
9d7d4601dc | ||
|
|
fdd741446c | ||
|
|
fdbab1685f | ||
|
|
ed0940b126 | ||
|
|
a087c7bb3a | ||
|
|
0439db2ec0 | ||
|
|
c1a5d4d336 | ||
|
|
eeffc6361a | ||
|
|
e2be3c20b7 |
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.13-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
|
||||
@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/l
|
||||
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
dpkg-reconfigure --frontend=noninteractive locales && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV TZ="America/Los_Angeles"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
38
README.md
38
README.md
@@ -5,6 +5,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||

|
||||
|
||||
## Key Features
|
||||

|
||||
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
@@ -28,7 +29,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **Wiki Integration**: Look up data using Wikipedia results.
|
||||
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
|
||||
- **Satalite Pass Info**: Get passes for satalite at your location.
|
||||
@@ -48,6 +49,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **UK.GOV Alerts**: Pulling data form the UK.GOV alert page
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
@@ -60,7 +62,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, or [femtofox](https://github.com/noon92/femtofox) project for embedding, possibly see the [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), there is also [femtofox](https://github.com/noon92/femtofox). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -145,6 +147,7 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
@@ -210,19 +213,17 @@ alert_interface = 1
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS
|
||||
This uses the SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. femaAlertBroadcastCh is currently not written, still under development.
|
||||
#### FEMA iPAWS/EAS and UK.gov
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. UK.gov for England
|
||||
|
||||
```ini
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = True
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2,4
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes trigger local alert. (e.g., SAME, FIPS, ZIP)
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAtest = True # Ignore any headline that includes the word Test
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
enableGBalerts = False # use UK.gov for alert source
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
@@ -287,6 +288,7 @@ file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
```
|
||||
|
||||
#### Offline EAS
|
||||
@@ -358,6 +360,7 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
@@ -370,7 +373,8 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts. Headline or expanded details | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
@@ -459,14 +463,6 @@ pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
@@ -478,3 +474,5 @@ To enable emoji in the Debian console, install the fonts:
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.
|
||||
|
||||
@@ -25,7 +25,7 @@ port = /dev/ttyUSB0
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# Allows auto-ping feature in a channel, False forces DM
|
||||
# Allows auto-ping feature in a channel, False forces to 1 ping only
|
||||
autoPingInChannel = False
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
|
||||
defaultChannel = 0
|
||||
@@ -108,33 +108,6 @@ bbslink_enabled = False
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
|
||||
# location module
|
||||
[location]
|
||||
enabled = True
|
||||
@@ -151,23 +124,28 @@ repeaterLookup = rbook
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
|
||||
# NOAA Hydrology unique identifiers, LID or USGS ID
|
||||
riverListDefault =
|
||||
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = False
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2
|
||||
# Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastEnabled = False
|
||||
# Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
enableGBalerts = False
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
@@ -208,6 +186,34 @@ file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
# only return a single random line from the news file
|
||||
news_random_line = False
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
|
||||
251
install.sh
251
install.sh
@@ -11,30 +11,49 @@ printf "\nThis script will try and install the Meshing Around Bot and its depend
|
||||
printf "Installer works best in raspian/debian/ubuntu, if there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# Check and install dependencies
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
|
||||
sudo apt-get install python3 python3-pip
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, trying 'apt-get install python3-pip'\n"
|
||||
sudo apt-get install python3-pip
|
||||
fi
|
||||
# check if running on embedded
|
||||
printf "\nAre You installing into an embedded system? (y/n)"
|
||||
read embedded
|
||||
|
||||
# double check for python3 and pip
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, please install python3 with your OS\n"
|
||||
exit 1
|
||||
if [ $embedded == "y" ]; then
|
||||
printf "\nDetected embedded skipping dependency installation\n"
|
||||
if [ $program_path != "/opt/meshing-around" ]; then
|
||||
printf "\nIt is suggested to project path to /opt/meshing-around\n"
|
||||
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
|
||||
read move
|
||||
if [ $move == "y" ]; then
|
||||
sudo mv $program_path /opt/meshing-around
|
||||
cd /opt/meshing-around
|
||||
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Check and install dependencies
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
|
||||
sudo apt-get install python3 python3-pip
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, trying 'apt-get install python3-pip'\n"
|
||||
sudo apt-get install python3-pip
|
||||
fi
|
||||
|
||||
# double check for python3 and pip
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, please install python3 with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\nDependencies installed\n"
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\nDependencies installed\n"
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
@@ -56,58 +75,63 @@ fi
|
||||
cp config.template config.ini
|
||||
printf "\nConfig files generated!\n"
|
||||
|
||||
printf "\nDo you want to install the bot in a python virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3/venv error, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "The Following could be messy, or take some time on slower devices."
|
||||
echo "Creating virtual environment..."
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
|
||||
sudo apt-get install python3-venv
|
||||
fi
|
||||
# create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# double check for python3-venv
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\nVirtual environment created\n"
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
# check if running on embedded
|
||||
if [ $embedded == "y" ]; then
|
||||
printf "\nDetected embedded skipping venv\n"
|
||||
else
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [ $rpi == "y" ]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
printf "\nDo you want to install the bot in a python virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3/venv error, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "The Following could be messy, or take some time on slower devices."
|
||||
echo "Creating virtual environment..."
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
|
||||
sudo apt-get install python3-venv
|
||||
fi
|
||||
# create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# double check for python3-venv
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\nVirtual environment created\n"
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [ $rpi == "y" ]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -134,58 +158,75 @@ sudo systemctl daemon-reload
|
||||
printf "\n service files updated\n"
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
echo "useradd -M meshbot"
|
||||
echo "usermod -L meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable pong_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "mesh" ]; then
|
||||
echo "useradd -M meshbot"
|
||||
echo "usermod -L meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
# install service for mesh bot
|
||||
sudo cp etc/mesh_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable mesh_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "n" ]; then
|
||||
if [ -f launch.sh ]; then
|
||||
printf "\nTo run the bot, use the command: ./launch.sh\n"
|
||||
./launch.sh
|
||||
# check if running on embedded
|
||||
if [ $embedded == "n" ]; then
|
||||
# ask if emoji font should be installed for linux
|
||||
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [ $gemma == "y" ]; then
|
||||
ollama pull gemma2:2b
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [ $gemma == "y" ]; then
|
||||
ollama pull gemma2:2b
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
|
||||
fi
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
if [ $venv == "y" ]; then
|
||||
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
|
||||
fi
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
else
|
||||
# we are on embedded
|
||||
# replace "type = serial" with "type = tcp" in config.ini
|
||||
replace="s|type = serial|type = tcp|g"
|
||||
sed -i "$replace" config.ini
|
||||
# replace "# hostname = 192.168.0.1" with "hostname = localhost" in config.ini
|
||||
replace="s|# hostname = 192.168.0.1|hostname = localhost|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nConfig file updated for embedded\n"
|
||||
|
||||
# Set up the meshing around service
|
||||
#sudo cp /opt/meshing-around/meshing-around.service /etc/systemd/system/meshing-around.service
|
||||
#sudo systemctl enable meshing-around.service
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
|
||||
130
mesh_bot.py
130
mesh_bot.py
@@ -14,8 +14,6 @@ restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golf
|
||||
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
|
||||
@@ -25,7 +23,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
message_lower = message.lower()
|
||||
bot_response = "🤖I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
# Command List
|
||||
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
|
||||
default_commands = {
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
@@ -45,8 +43,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"ealert": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"email:": lambda: handle_email(message_from_id, message),
|
||||
"games": lambda: gamesCmdList,
|
||||
"globalthermonuclearwar": lambda: handle_gTnW(),
|
||||
@@ -64,6 +62,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"readnews": lambda: read_news(),
|
||||
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
|
||||
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
|
||||
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
|
||||
"setemail": lambda: handle_email(message_from_id, message),
|
||||
@@ -72,6 +71,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"sms:": lambda: handle_sms(message_from_id, message),
|
||||
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
@@ -182,35 +182,36 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
# disabled in channel
|
||||
if autoPingInChannel and not isDM:
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
msg = "🔊AutoPing via DM only⛔️"
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not isDM:
|
||||
msg = get_name_from_number(message_from_id) + msg
|
||||
if not useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
|
||||
@@ -281,9 +282,9 @@ def handle_wxalert(message_from_id, deviceID, message):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if "wxalert" in message:
|
||||
# Detailed weather alert
|
||||
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
|
||||
weatherAlert = getActiveWeatherAlertsDetailNOAA(str(location[0]), str(location[1]))
|
||||
else:
|
||||
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
|
||||
weatherAlert = getWeatherAlertsNOAA(str(location[0]), str(location[1]))
|
||||
|
||||
if NO_ALERTS not in weatherAlert:
|
||||
weatherAlert = weatherAlert[0]
|
||||
@@ -645,29 +646,56 @@ def handleGolf(message, nodeID, deviceID):
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handle_riverFlow(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
userRiver = message.lower()
|
||||
|
||||
if "riverflow " in userRiver:
|
||||
userRiver = userRiver.split("riverflow ")[1] if "riverflow " in userRiver else riverListDefault
|
||||
else:
|
||||
userRiver = userRiver.split(",") if "," in userRiver else riverListDefault
|
||||
|
||||
# return river flow data
|
||||
if use_meteo_wxApi:
|
||||
return get_flood_openmeteo(location[0], location[1])
|
||||
else:
|
||||
# if userRiver a list
|
||||
if type(userRiver) == list:
|
||||
msg = ""
|
||||
for river in userRiver:
|
||||
msg += get_flood_noaa(location[0], location[1], river)
|
||||
return msg
|
||||
# if single river
|
||||
msg = get_flood_noaa(location[0], location[1], userRiver)
|
||||
return msg
|
||||
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
|
||||
logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
|
||||
#logger.debug("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("System: Bot Returning Open-Meteo API for weather metric")
|
||||
#logger.debug("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 cmd or use_metric:
|
||||
logger.debug("System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_weather(str(location[0]), str(location[1]), 1)
|
||||
#logger.debug("System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_NOAAweather(str(location[0]), str(location[1]), 1)
|
||||
else:
|
||||
logger.debug("System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_weather(str(location[0]), str(location[1]))
|
||||
#logger.debug("System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_NOAAweather(str(location[0]), str(location[1]))
|
||||
return weather
|
||||
|
||||
def handle_fema_alerts(message, message_from_id, deviceID):
|
||||
def handle_emergency_alerts(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if enableGBalerts:
|
||||
# UK Alerts
|
||||
return get_govUK_alerts(str(location[0]), str(location[1]))
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert
|
||||
# Detailed alert FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
else:
|
||||
# Headlines only
|
||||
# Headlines only FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
@@ -736,6 +764,12 @@ def handle_sun(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
|
||||
@@ -826,7 +860,7 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
|
||||
|
||||
def handle_tide(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_tide(str(location[0]), str(location[1]))
|
||||
return get_NOAAtide(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_moon(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
@@ -978,7 +1012,7 @@ def onReceive(packet, interface):
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
@@ -1008,6 +1042,10 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
snr = packet.get('rxSnr', 0)
|
||||
@@ -1179,7 +1217,7 @@ def onReceive(packet, interface):
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
@@ -1240,6 +1278,8 @@ async def start_rx():
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if enableSMTP:
|
||||
|
||||
@@ -3,22 +3,31 @@
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import random
|
||||
import os
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
def read_file(file_monitor_file_path):
|
||||
def read_file(file_monitor_file_path, random_line_only=False):
|
||||
try:
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
return random.choice(lines)
|
||||
else:
|
||||
# read the whole file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path)
|
||||
return read_file(news_file_path, read_news_enabled)
|
||||
|
||||
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
|
||||
73
modules/gpio.py
Normal file
73
modules/gpio.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# GPIO module for MeshLink, concept code, not implemented
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
# https://pypi.org/project/gpio/
|
||||
#import gpio
|
||||
|
||||
# https://pythonhosted.org/RPIO/
|
||||
import RPIO
|
||||
|
||||
from modules.log import *
|
||||
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
|
||||
|
||||
# set up input channel without pull-up
|
||||
RPIO.setup(7, RPIO.IN)
|
||||
|
||||
# set up input channel with pull-up
|
||||
RPIO.setup(8, RPIO.IN, pull_up_down=RPIO.PUD_UP)
|
||||
|
||||
# set up GPIO output channel
|
||||
RPIO.setup(8, RPIO.OUT)
|
||||
|
||||
# change to BOARD numbering schema
|
||||
RPIO.setmode(RPIO.BOARD)
|
||||
|
||||
# set up PWM channel
|
||||
RPIO.setup(12, RPIO.OUT)
|
||||
p = RPIO.PWM(12)
|
||||
|
||||
def gpio_status():
|
||||
# get status of GPIO pins
|
||||
gpio_status = ""
|
||||
gpio_status += "GPIO 7: " + str(RPIO.input(7)) + "\n"
|
||||
gpio_status += "GPIO 8: " + str(RPIO.input(8)) + "\n"
|
||||
gpio_status += "GPIO 12: " + str(RPIO.input(12)) + "\n"
|
||||
return gpio_status
|
||||
|
||||
def gpio_toggle():
|
||||
# toggle GPIO pin 8
|
||||
RPIO.output(8, not RPIO.input(8))
|
||||
return "GPIO 8 toggled"
|
||||
|
||||
def gpio_pwm():
|
||||
# set PWM on GPIO pin 12
|
||||
p.start(50)
|
||||
return "PWM started"
|
||||
|
||||
def gpio_stop():
|
||||
# stop PWM on GPIO pin 12
|
||||
p.stop()
|
||||
return "PWM stopped"
|
||||
|
||||
def gpio_shutdown():
|
||||
# shutdown GPIO
|
||||
RPIO.cleanup()
|
||||
return "GPIO shutdown"
|
||||
|
||||
def trap_gpio(message):
|
||||
# trap for GPIO commands
|
||||
if "status" in message:
|
||||
return gpio_status()
|
||||
elif "toggle" in message:
|
||||
return gpio_toggle()
|
||||
elif "pwm" in message:
|
||||
return gpio_pwm()
|
||||
elif "stop" in message:
|
||||
return gpio_stop()
|
||||
elif "shutdown" in message:
|
||||
return gpio_shutdown()
|
||||
else:
|
||||
return "GPIO command not recognized"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# helper functions to use location data like NOAA weather
|
||||
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert")
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -154,9 +154,8 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
else:
|
||||
msg = f"no results.. sorry"
|
||||
return msg
|
||||
|
||||
|
||||
def get_tide(lat=0, lon=0):
|
||||
def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
@@ -172,7 +171,7 @@ def get_tide(lat=0, lon=0):
|
||||
|
||||
if station_json['stationList'] == [] or station_json['stationList'] is None:
|
||||
logger.error("Location:No tide station found")
|
||||
return ERROR_FETCHING_DATA
|
||||
return "No tide station found with info provided"
|
||||
|
||||
station_id = station_json['stationList'][0]['stationId']
|
||||
|
||||
@@ -219,7 +218,7 @@ def get_tide(lat=0, lon=0):
|
||||
tide_table = tide_table[:-1]
|
||||
return tide_table
|
||||
|
||||
def get_weather(lat=0, lon=0, unit=0):
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
weather = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -263,7 +262,7 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
weather = weather[:-1]
|
||||
|
||||
# get any alerts and return the count
|
||||
alerts = getWeatherAlerts(lat, lon)
|
||||
alerts = getWeatherAlertsNOAA(lat, lon)
|
||||
|
||||
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
|
||||
alert = ""
|
||||
@@ -333,7 +332,7 @@ def abbreviate_noaa(row):
|
||||
|
||||
return line
|
||||
|
||||
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
@@ -381,26 +380,27 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCache = ""
|
||||
def alertBrodcast():
|
||||
wxAlertCacheNOAA = ""
|
||||
def alertBrodcastNOAA():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCache
|
||||
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
|
||||
global wxAlertCacheNOAA
|
||||
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
|
||||
# check if any reason to discard the alerts
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
|
||||
return False
|
||||
elif currentAlert == NO_ALERTS:
|
||||
wxAlertCache = ""
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] != wxAlertCache:
|
||||
elif currentAlert[0] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
logger.debug("Location:Broadcasting weather alerts")
|
||||
wxAlertCache = currentAlert[0]
|
||||
wxAlertCacheNOAA = currentAlert[0]
|
||||
return currentAlert
|
||||
|
||||
return False
|
||||
|
||||
def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -560,3 +560,53 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
alert = NO_ALERTS
|
||||
|
||||
return alert
|
||||
|
||||
def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
# get the latest flood alert from NOAA
|
||||
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
|
||||
headers = {'accept': 'application/json'}
|
||||
if uid == 0:
|
||||
return "No flood gauge data found"
|
||||
try:
|
||||
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
|
||||
if not response.ok:
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
data = response.json()
|
||||
if not data:
|
||||
return "No flood gauge data found"
|
||||
|
||||
# extract values from JSON
|
||||
try:
|
||||
name = data['name']
|
||||
status_observed_primary = data['status']['observed']['primary']
|
||||
status_observed_primary_unit = data['status']['observed']['primaryUnit']
|
||||
status_observed_secondary = data['status']['observed']['secondary']
|
||||
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
|
||||
status_observed_floodCategory = data['status']['observed']['floodCategory']
|
||||
status_forecast_primary = data['status']['forecast']['primary']
|
||||
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
|
||||
status_forecast_secondary = data['status']['forecast']['secondary']
|
||||
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
|
||||
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
|
||||
|
||||
# except KeyError as e:
|
||||
# print(f"Missing key in data: {e}")
|
||||
# except TypeError as e:
|
||||
# print(f"Type error in data: {e}")
|
||||
except Exception as e:
|
||||
logger.warning("Location:Error extracting flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# format the flood data
|
||||
logger.debug(f"System: NOAA Flood data for {str(uid)}")
|
||||
flood_data = f"Flood Data {name}:\n"
|
||||
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
|
||||
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
|
||||
|
||||
return flood_data
|
||||
|
||||
|
||||
60
modules/locationdata_eu.py
Normal file
60
modules/locationdata_eu.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# helper functions to use location data for data outside US/north america
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
from geopy.geocoders import Nominatim # pip install geopy
|
||||
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.log import *
|
||||
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
|
||||
def get_govUK_alerts(shortAlerts=False):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
|
||||
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK alerts: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
if alert:
|
||||
return "🚨" + alert.get_text(strip=True)
|
||||
else:
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_wxUKgov():
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
|
||||
try:
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.content, 'xml')
|
||||
|
||||
items = soup.find_all('item')
|
||||
alerts = []
|
||||
|
||||
for item in items:
|
||||
title = item.find('title').get_text(strip=True)
|
||||
description = item.find('description').get_text(strip=True)
|
||||
alerts.append(f"🚨 {title}: {description}")
|
||||
|
||||
return "\n".join(alerts) if alerts else NO_ALERTS
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK weather warnings: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
@@ -28,6 +28,8 @@ retry_int2 = False
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
playingGame = False
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -154,25 +156,22 @@ try:
|
||||
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
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
femaAlertBroadcastEnabled = config['location'].getboolean('femaAlertBroadcastEnabled', False) # default False
|
||||
femaAlertBroadcastCh = config['location'].get('femaAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
|
||||
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
if ',' in wxAlertBroadcastChannel:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
|
||||
# location alerts
|
||||
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
@@ -221,6 +220,7 @@ try:
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
|
||||
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
|
||||
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
|
||||
@@ -9,7 +9,7 @@ import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond", "satpass")
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
|
||||
|
||||
def hf_band_conditions():
|
||||
# ham radio HF band conditions
|
||||
|
||||
@@ -18,7 +18,6 @@ asyncLoop = asyncio.new_event_loop()
|
||||
games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
# ping, pinging, ack, testing, test, pong
|
||||
@@ -28,9 +27,9 @@ if ping_enabled:
|
||||
|
||||
# Sitrep Configuration
|
||||
if sitrep_enabled:
|
||||
trap_list_sitrep = ("sitrep", "lheard")
|
||||
trap_list_sitrep = ("sitrep", "lheard", "sysinfo")
|
||||
trap_list = trap_list + trap_list_sitrep
|
||||
help_message = help_message + ", sitrep"
|
||||
help_message = help_message + ", sitrep, sysinfo"
|
||||
|
||||
# MOTD Configuration
|
||||
if motd_enabled:
|
||||
@@ -75,6 +74,10 @@ 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, rlist"
|
||||
if enableGBalerts:
|
||||
from modules.locationdata_eu import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_eu
|
||||
#help_message = help_message + ", ukalert, ukwx, ukflood"
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
@@ -191,7 +194,7 @@ if file_monitor_enabled or read_news_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
help_message = help_message + ", readmail"
|
||||
help_message = help_message + ", readnews"
|
||||
|
||||
# clean up the help message
|
||||
help_message = help_message.split(", ")
|
||||
@@ -662,7 +665,7 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
break
|
||||
|
||||
|
||||
def handleWxBroadcast(deviceID=1):
|
||||
def handleAlertBroadcast(deviceID=1):
|
||||
# only allow API call every 20 minutes
|
||||
# the watchdog will call this function 3 times, seeing possible throttling on the API
|
||||
clock = datetime.now()
|
||||
@@ -672,15 +675,50 @@ def handleWxBroadcast(deviceID=1):
|
||||
return False
|
||||
|
||||
# check for alerts
|
||||
alert = alertBrodcast()
|
||||
if alert:
|
||||
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(msg, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
alertWx = alertBrodcastNOAA()
|
||||
alertFema = getIpawsAlert(latitudeValue,longitudeValue, shortAlerts=True)
|
||||
|
||||
if enableGBalerts:
|
||||
alertUk = get_govUK_alerts()
|
||||
else:
|
||||
alertUk = NO_ALERTS
|
||||
|
||||
# format alert
|
||||
if alertWx:
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
|
||||
else:
|
||||
wxAlert = False
|
||||
|
||||
femaAlert = alertFema
|
||||
ukAlert = alertUk
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if NO_ALERTS not in femaAlert:
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(femaAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
if NO_ALERTS not in ukAlert:
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(ukAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for 10 seconds
|
||||
time.sleep(10)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
if wxAlert:
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(wxAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
@@ -743,7 +781,7 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
|
||||
# Create a StringIO object to capture the
|
||||
output_capture = io.StringIO()
|
||||
with contextlib.redirect_stdout(output_capture):
|
||||
with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture):
|
||||
interface.localNode.getMetadata()
|
||||
console_output = output_capture.getvalue()
|
||||
if "firmware_version" in console_output:
|
||||
@@ -751,22 +789,22 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
return fwVer
|
||||
return -1
|
||||
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0):
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
interface = interface1 if rxNode == 1 else interface2
|
||||
global telemetryData
|
||||
|
||||
# throttle the telemetry requests to prevent spamming the device
|
||||
if rxNode == 1:
|
||||
if time.time() - telemetryData[0]['interface1'] < 600:
|
||||
if time.time() - telemetryData[0]['interface1'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface1'] = time.time()
|
||||
elif rxNode == 2:
|
||||
if time.time() - telemetryData[0]['interface2'] < 600:
|
||||
if time.time() - telemetryData[0]['interface2'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface2'] = time.time()
|
||||
|
||||
# some telemetry data is not available in python-meshtastic?
|
||||
# bring in values from the last telemetry request for the node
|
||||
# bring in values from the last telemetry dump for the node
|
||||
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
|
||||
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
|
||||
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
|
||||
@@ -901,7 +939,20 @@ def consumeMetadata(packet, rxNode=0):
|
||||
if debugMetadata: print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
|
||||
# get the remote hardware data
|
||||
remote_hardware_data = packet['decoded']['remoteHardware']
|
||||
|
||||
|
||||
def get_sysinfo(nodeID=0, deviceID=1):
|
||||
# Get the system telemetry data for return on the sysinfo command
|
||||
sysinfo = ''
|
||||
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
|
||||
if "numPacketsRx:0" in stats or stats == -1:
|
||||
return "Gathering Telemetry try again later⏳"
|
||||
# replace Telemetry with Int in string
|
||||
stats = stats.replace("Telemetry", "Int")
|
||||
sysinfo += f"📊{stats}"
|
||||
if interface2_enabled:
|
||||
sysinfo += f"📊{stats}"
|
||||
|
||||
return sysinfo
|
||||
|
||||
async def BroadcastScheduler():
|
||||
# handle schedule checks for the broadcast of messages
|
||||
@@ -1074,8 +1125,10 @@ async def watchdog():
|
||||
# multiPing handler
|
||||
handleMultiPing(0,1)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(1)
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(1)
|
||||
|
||||
# Telemetry data
|
||||
int1Data = displayNodeTelemetry(0, 1)
|
||||
@@ -1104,10 +1157,12 @@ async def watchdog():
|
||||
await handleSentinel(2)
|
||||
|
||||
# multiPing handler
|
||||
handleMultiPing(0,2)
|
||||
handleMultiPing(0,1)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(2)
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(1)
|
||||
|
||||
# Telemetry data
|
||||
int2Data = displayNodeTelemetry(0, 2)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import openmeteo_requests # pip install openmeteo-requests
|
||||
from retry_requests import retry # pip install retry_requests
|
||||
#import requests_cache
|
||||
#import openmeteo_requests # pip install openmeteo-requests
|
||||
#from retry_requests import retry # pip install retry_requests
|
||||
|
||||
import requests
|
||||
import json
|
||||
from modules.log import *
|
||||
|
||||
def get_weather_data(api_url, params):
|
||||
response = requests.get(api_url, params=params)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
return response.json()
|
||||
|
||||
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"
|
||||
@@ -34,27 +35,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
try:
|
||||
# Fetch the weather data
|
||||
responses = openmeteo.weather_api(url, params=params)
|
||||
weather_data = get_weather_data(url, 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 location
|
||||
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = weather_data
|
||||
|
||||
# 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()
|
||||
daily = response['daily']
|
||||
daily_weather_code = daily['weather_code']
|
||||
daily_temperature_2m_max = daily['temperature_2m_max']
|
||||
daily_temperature_2m_min = daily['temperature_2m_min']
|
||||
daily_precipitation_hours = daily['precipitation_hours']
|
||||
daily_precipitation_probability_max = daily['precipitation_probability_max']
|
||||
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
|
||||
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
|
||||
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
@@ -191,3 +194,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather_report
|
||||
|
||||
def get_flood_openmeteo(lat=0, lon=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Flood data
|
||||
url = "https://flood-api.open-meteo.com/v1/flood"
|
||||
params = {
|
||||
"latitude": {lat},
|
||||
"longitude": {lon},
|
||||
"timezone": "auto",
|
||||
"daily": "river_discharge",
|
||||
"forecast_days": forecastDays
|
||||
}
|
||||
|
||||
try:
|
||||
# Fetch the flood data
|
||||
flood_data = get_weather_data(url, params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = flood_data
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response['daily']
|
||||
daily_river_discharge = daily['river_discharge']
|
||||
# check if none
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# create a flood report
|
||||
flood_report = ""
|
||||
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
|
||||
|
||||
return flood_report
|
||||
|
||||
72
pong_bot.py
72
pong_bot.py
@@ -9,7 +9,6 @@ from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
@@ -18,6 +17,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
@@ -29,6 +29,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
}
|
||||
@@ -100,35 +101,36 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
# disabled in channel
|
||||
if autoPingInChannel and not isDM:
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
msg = "🔊AutoPing via DM only⛔️"
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not isDM:
|
||||
msg = get_name_from_number(message_from_id) + msg
|
||||
if not useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
|
||||
@@ -140,6 +142,12 @@ def handle_motd(message):
|
||||
return "MOTD Set to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
@@ -200,6 +208,9 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
@@ -209,7 +220,10 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
@@ -333,7 +347,7 @@ def onReceive(packet, interface):
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
@@ -6,9 +6,6 @@ requests
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
|
||||
Reference in New Issue
Block a user