mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850ee2d291 | ||
|
|
cefbe93178 | ||
|
|
44b2837ba0 | ||
|
|
1aa6a7a41a | ||
|
|
7abd1fd704 | ||
|
|
d35832caa8 | ||
|
|
b4b0f2c561 | ||
|
|
1e8ff95769 | ||
|
|
41093be614 | ||
|
|
6fe874e192 | ||
|
|
3fa5d96073 | ||
|
|
242c1c8741 | ||
|
|
c679cee66c | ||
|
|
ca896c0f35 | ||
|
|
e3cd727cc3 | ||
|
|
ded8470677 | ||
|
|
f0b63b8b20 | ||
|
|
8a9c7a1147 | ||
|
|
2d0e6b54b3 | ||
|
|
0745847d3a | ||
|
|
6c49c5c87f | ||
|
|
719fa95c1c | ||
|
|
b642961d26 | ||
|
|
f59d97f6ad | ||
|
|
92d5f01ce5 | ||
|
|
39e53eb599 | ||
|
|
9f6165503e | ||
|
|
ec27ab65da | ||
|
|
f34eefb75a | ||
|
|
ce2ccb1455 | ||
|
|
da144a2b89 | ||
|
|
bbdccb382a | ||
|
|
95f75b8e0a | ||
|
|
0bf4915cd5 | ||
|
|
f83793acc9 | ||
|
|
abb2fa6b61 | ||
|
|
6d90d6f207 | ||
|
|
9c9e9a02e6 | ||
|
|
80fc795f35 | ||
|
|
166c49854f | ||
|
|
a685fc3a9b | ||
|
|
91da1a4c58 | ||
|
|
9889fd0da8 | ||
|
|
bebd9352ea | ||
|
|
fd1cd2a44c | ||
|
|
ac55a51c87 | ||
|
|
86144cd888 | ||
|
|
d7a37ce9f1 | ||
|
|
da7035dfed | ||
|
|
da500981a2 | ||
|
|
b4dc2207a6 | ||
|
|
b69a187466 | ||
|
|
66d143d68e | ||
|
|
49f2dcff88 | ||
|
|
2c3c3fed10 | ||
|
|
2872fb040e | ||
|
|
6097ff899c | ||
|
|
aec75d598a | ||
|
|
0640fdbbae | ||
|
|
8312f4e683 | ||
|
|
990ea4f4e4 | ||
|
|
9acf9df3bb | ||
|
|
37942e950e | ||
|
|
8a0e1cba7c | ||
|
|
8d0a53ec3e | ||
|
|
2ea3917eba | ||
|
|
75410c98e3 | ||
|
|
10171a712e | ||
|
|
fa76a76203 | ||
|
|
e0e275a49c | ||
|
|
bf39c2f088 | ||
|
|
34d36057c1 | ||
|
|
4e1d1de883 | ||
|
|
97f103dfd7 | ||
|
|
47089871b1 | ||
|
|
cc7ef129f6 | ||
|
|
0fa5d06a3a | ||
|
|
7fc44ec06e | ||
|
|
184760096e | ||
|
|
8868d10388 | ||
|
|
1ce2ecd75c | ||
|
|
69e1c21488 | ||
|
|
97a2ffce7b | ||
|
|
4c0d3a597e | ||
|
|
094f7e61a0 | ||
|
|
a54ecaa5a1 | ||
|
|
bd12392d69 | ||
|
|
882bcf3f4b | ||
|
|
c0d0ca3743 | ||
|
|
d74d848646 | ||
|
|
2afb915b56 | ||
|
|
d5e48bead1 | ||
|
|
3c80848f61 | ||
|
|
64345fe47a | ||
|
|
32f734d69b | ||
|
|
aa6de00c5b | ||
|
|
6df4ba5756 | ||
|
|
a11a2780db | ||
|
|
980414f872 | ||
|
|
f26334d625 | ||
|
|
24546b28d6 | ||
|
|
f33da848cd | ||
|
|
57ce15de4e | ||
|
|
b8886e0662 | ||
|
|
9a1e86f25e | ||
|
|
fa8021ab5a | ||
|
|
f3917f1c3d | ||
|
|
c1443048fd | ||
|
|
da430557f3 | ||
|
|
84152bda65 | ||
|
|
b6e80ae576 | ||
|
|
18ac26864c | ||
|
|
b661fbc750 | ||
|
|
3049d18663 |
92
INSTALL.md
92
INSTALL.md
@@ -2,6 +2,9 @@
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Manual Install](#manual-install)
|
||||
- [Docker Installation](#docker-installation)
|
||||
- [Requirements](#requirements)
|
||||
- [install.sh](#installsh)
|
||||
- [Purpose](#purpose)
|
||||
- [Usage](#usage)
|
||||
@@ -22,68 +25,69 @@
|
||||
|
||||
---
|
||||
|
||||
### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
## Manual Install
|
||||
|
||||
Install all required dependencies using pip:
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
Copy the configuration template and edit as needed:
|
||||
|
||||
```sh
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker Installation - handy for windows
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
## Docker Installation
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
See [script/docker/README.md](script/docker/README.md) for Docker-based setup instructions.
|
||||
Docker is recommended for Windows or if you want an isolated environment.
|
||||
|
||||
Mesh-bot enhancements:
|
||||
---
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
## Requirements
|
||||
|
||||
For the Ollama LLM:
|
||||
- **Python 3.8 or later** (Python 3.13+ supported in Docker)
|
||||
- All dependencies are listed in `requirements.txt` and can be installed with:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
- To enable emoji in the Debian/Ubuntu console:
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
- For Ollama LLM support, see the prompts during `install.sh` or visit [https://ollama.com](https://ollama.com).
|
||||
|
||||
```sh
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
---
|
||||
|
||||
## install.sh
|
||||
|
||||
### Purpose
|
||||
`install.sh` is an installation and setup script for the Meshing Around Bot project. It automates installing dependencies, configuring the environment, setting up system services, and preparing the bot for use on Linux systems (especially Debian/Ubuntu/Raspberry Pi and embedded devices).
|
||||
|
||||
`install.sh` automates installation, configuration, and service setup for the Meshing Around Bot project. It is designed for Linux systems (Debian/Ubuntu/Raspberry Pi and embedded devices).
|
||||
|
||||
### Usage
|
||||
Run this script from the project root directory:
|
||||
|
||||
Run from the project root directory:
|
||||
|
||||
```sh
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
To uninstall:
|
||||
|
||||
```sh
|
||||
bash install.sh --nope
|
||||
```
|
||||
|
||||
### What it does
|
||||
- Checks for existing installations and required permissions.
|
||||
- Optionally moves the project to `/opt/meshing-around` for standardization.
|
||||
- Installs Python and pip if not present (unless on embedded systems).
|
||||
|
||||
- Checks for existing installations and permissions.
|
||||
- Optionally moves the project to `/opt/meshing-around`.
|
||||
- Installs Python and pip if missing (unless on embedded systems).
|
||||
- Adds the current user (or a dedicated `meshbot` user) to necessary groups for serial and Bluetooth access.
|
||||
- Copies and configures systemd service files for running the bot as a service.
|
||||
- Sets up configuration files, updating latitude/longitude automatically.
|
||||
@@ -95,11 +99,13 @@ bash install.sh
|
||||
- Offers to reboot the system to complete setup.
|
||||
|
||||
### When to use
|
||||
|
||||
- For first-time installation of the Meshing Around Bot.
|
||||
- When migrating to a new device or environment.
|
||||
- After cloning or updating the repository to set up dependencies and services.
|
||||
|
||||
### Note
|
||||
|
||||
- You may be prompted for input during installation (e.g., for embedded mode, virtual environment, or optional features).
|
||||
- Review and edit the script if you have custom requirements or are running on a non-standard system.
|
||||
|
||||
@@ -108,10 +114,13 @@ bash install.sh
|
||||
## update.sh
|
||||
|
||||
### Purpose
|
||||
|
||||
`update.sh` is an update and maintenance script for the Meshing Around Bot project. It automates the process of safely updating your codebase, backing up data, and merging configuration changes.
|
||||
|
||||
### Usage
|
||||
Run this script from the project root directory:
|
||||
|
||||
Run from the project root directory:
|
||||
|
||||
```sh
|
||||
bash update.sh
|
||||
```
|
||||
@@ -122,6 +131,7 @@ chmod +x update.sh
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
- Stops running Mesh Bot services to prevent conflicts during update.
|
||||
- Fetches and pulls the latest changes from the GitHub repository (using `git pull --rebase`).
|
||||
- Handles git conflicts, offering to reset to the latest remote version if needed.
|
||||
@@ -132,10 +142,12 @@ chmod +x update.sh
|
||||
- Provides status messages and logs for troubleshooting.
|
||||
|
||||
### When to use
|
||||
|
||||
- To update your Mesh Bot installation to the latest version.
|
||||
- Before making significant changes or troubleshooting, as it creates a backup of your data.
|
||||
|
||||
### Note
|
||||
|
||||
- Review `ini_merge_log.txt` and `config_new.ini` after running for any configuration changes or errors.
|
||||
- You may be prompted if git conflicts are detected.
|
||||
|
||||
@@ -144,9 +156,11 @@ chmod +x update.sh
|
||||
## launch.sh
|
||||
|
||||
### Purpose
|
||||
|
||||
`launch.sh` is a convenience script for starting the Mesh Bot, Pong Bot, or generating reports within the Python virtual environment. It ensures the correct environment is activated and the appropriate script is run.
|
||||
|
||||
### How to Use
|
||||
|
||||
From your project root, run one of the following commands:
|
||||
|
||||
- Launch Mesh Bot:
|
||||
@@ -171,6 +185,7 @@ From your project root, run one of the following commands:
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
- Ensures you are in the project directory.
|
||||
- Copies `config.template` to `config.ini` if no config exists.
|
||||
- Activates the Python virtual environment (`venv`).
|
||||
@@ -178,6 +193,7 @@ From your project root, run one of the following commands:
|
||||
- Deactivates the virtual environment when done.
|
||||
|
||||
### Note
|
||||
|
||||
- The script requires a Python virtual environment (`venv`) to be present in the project directory.
|
||||
- If `venv` is missing, the script will exit with an error message.
|
||||
- Always provide an argument (`mesh`, `pong`, `html`, `html5`, or `add`) to specify what you want to launch.
|
||||
25
README.md
25
README.md
@@ -158,21 +158,18 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
### Special Thanks
|
||||
- **xdep**: For the reporting tools.
|
||||
- **Nestpebble**: For new ideas and enhancements.
|
||||
- **mrpatrick1991**: For Docker configurations.
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
|
||||
For testing and feature ideas on Discord and GitHub, if its stable its thanks to you all.
|
||||
- **PiDiBi, Cisien, bitflip, nagu, Nestpebble, NomDeTom, Iris, Josh, GlockTuber, FJRPiolt, dj505, Woof, propstg, snydermesh, trs2982, F0X, Malice, mesb1, Hailo1999**
|
||||
- **xdep**: For the reporting html. 📊
|
||||
- **mrpatrick1991**: For OG Docker configurations. 💻
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and 🚨
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **dj505**: trying it on windows!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Iris**: testing and finding 🐞
|
||||
- **FJRPiolt**: testing bugs out!!
|
||||
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, Malice, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
- **WH6GXZ nurse dude**: Volcano Alerts 🌋
|
||||
- **mikecarper**: hamtest, leading to quiz etc.. 📋
|
||||
- **c.merphy360**: high altitude alerts. 🚀
|
||||
- **G7KSE**: DX Spotting idea. 📻
|
||||
- **Growing List of GitHub Contributers**
|
||||
- **Meshtastic Discord Community**: For putting up with 🥔
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
10
compose.yaml
10
compose.yaml
@@ -19,6 +19,16 @@ services:
|
||||
networks:
|
||||
- meshing-around-network
|
||||
|
||||
test-bot:
|
||||
image: ghcr.io/spudgunman/meshing-around:main
|
||||
container_name: test-bot
|
||||
command: ["/bin/bash", "-c", "python3 modules/test_bot.py | tee /tmp/test_tmp.txt; if grep -E 'failures=|errors=' /tmp/test_tmp.txt; then cp /tmp/test_tmp.txt /app/test_results.txt; fi"]
|
||||
volumes:
|
||||
- .:/app:rw
|
||||
networks:
|
||||
- meshing-around-network
|
||||
stdin_open: true
|
||||
|
||||
debug-console:
|
||||
image: ghcr.io/spudgunman/meshing-around:main
|
||||
container_name: debug-console
|
||||
|
||||
@@ -57,9 +57,9 @@ spaceWeather = True
|
||||
|
||||
# enable or disable the RSS module, and truncate the story
|
||||
rssEnable = True
|
||||
rssFeedURL = http://www.hackaday.com/rss.xml,http://rss.slashdot.org/Slashdot/slashdotMain
|
||||
rssFeedURL = http://www.hackaday.com/rss.xml,http://rss.slashdot.org/Slashdot/slashdotMain,http://www.reddit.com/r/meshtastic/.rss
|
||||
# RSS feed names must match the order of the URLs above, default is used if no match
|
||||
rssFeedNames = default,slashdot
|
||||
rssFeedNames = default,slashdot,mesh
|
||||
rssMaxItems = 3
|
||||
rssTruncate = 100
|
||||
|
||||
@@ -287,8 +287,8 @@ message = "MeshBot says Hello! DM for more info."
|
||||
# enable overides the above and uses the motd as the message
|
||||
schedulerMotd = False
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) or link (hour) for special auto messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
# value can also be 'joke' (min/interval) or 'weather' (time/day) or 'link' (hour/interval) for special auto messages
|
||||
# 'custom' for module/scheduler.py custom schedule examples
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
@@ -299,6 +299,7 @@ time =
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
dxspotter_enabled = True
|
||||
# device interface to send the message to
|
||||
sigWatchBroadcastInterface = 1
|
||||
# broadcast channel can also be a comma separated list of channels
|
||||
|
||||
@@ -1,52 +1,58 @@
|
||||
import schedule
|
||||
from modules.log import logger
|
||||
from modules.settings import MOTD
|
||||
from modules.system import send_message
|
||||
|
||||
def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc, MOTD, schedulerChannel, schedulerInterface):
|
||||
# custom scheduler job to run the schedule see examples below
|
||||
logger.debug(f"System: Starting the custom_scheduler.py default to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
"""
|
||||
Set up all custom schedules. Edit this function to add or remove scheduled tasks.
|
||||
"""
|
||||
|
||||
### Example schedules
|
||||
# Send a joke every 2 minutes
|
||||
#logger.debug(f"System: Custom Scheduler: Send a joke every 2 minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
#schedule.every(2).minutes.do(send_joke, send_message, tell_joke, schedulerChannel, schedulerInterface)
|
||||
# Send a good morning message every day at 9 AM
|
||||
#schedule.every().day.at("09:00").do(send_good_morning, send_message, schedulerChannel, schedulerInterface)
|
||||
# Send weather update every day at 8 AM
|
||||
#schedule.every().day.at("08:00").do(send_wx, send_message, handle_wxc, schedulerChannel, schedulerInterface)
|
||||
# Send weather alerts every Wednesday at noon
|
||||
#schedule.every().wednesday.at("12:00").do(send_weather_alert, send_message, schedulerChannel, schedulerInterface)
|
||||
# Send configuration URL every 2 days at 10 AM
|
||||
#schedule.every(2).days.at("10:00").do(send_config_url, send_message, schedulerChannel, schedulerInterface)
|
||||
# Send net starting message every Wednesday at 7 PM
|
||||
#schedule.every().wednesday.at("19:00").do(send_net_starting, send_message, schedulerChannel, schedulerInterface)
|
||||
# Send welcome message every 2 days at 8 AM
|
||||
#schedule.every(2).days.at("08:00").do(send_welcome, send_message, schedulerChannel, schedulerInterface)
|
||||
# Send MOTD every day at 1 PM
|
||||
#schedule.every().day.at("13:00").do(send_motd, send_message, MOTD, schedulerChannel, schedulerInterface)
|
||||
# Send bbslink message every 2 days at 10 AM
|
||||
#schedule.every(2).days.at("10:00").do(send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
# Example task functions, modify as needed the channel and interface parameters default to schedulerChannel and schedulerInterface
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#logger.debug("System: Custom Scheduler: Send WX every Morning at 08:00")
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
def send_joke(send_message, tell_joke, channel, interface):
|
||||
send_message(tell_joke(), channel, 0, interface)
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
def send_good_morning(send_message, channel, interface):
|
||||
send_message("Good Morning", channel, 0, interface)
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#logger.debug("System: Custom Scheduler: Config URL for Medium Fast Network Use every other day at 10:00")
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
def send_wx(send_message, handle_wxc, channel, interface):
|
||||
send_message(handle_wxc(0, 1, 'wx', days=1), channel, 0, interface)
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
def send_weather_alert(send_message, channel, interface):
|
||||
send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", channel, 0, interface)
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
|
||||
def send_config_url(send_message, channel, interface):
|
||||
send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", channel, 0, interface)
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00
|
||||
#logger.debug(f"System: Custom Scheduler: Welcome Notice for group on the 15th and 25th of the month at 12:00 on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", schedulerChannel, 0, schedulerInterface)).day(15, 25)
|
||||
def send_net_starting(send_message, channel, interface):
|
||||
send_message("Net Starting Now", channel, 0, interface)
|
||||
|
||||
# Send a joke every 6 hours
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
def send_welcome(send_message, channel, interface):
|
||||
send_message("Welcome to the group", channel, 0, interface)
|
||||
|
||||
# Send the Welcome Message every other day at 08:00
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, schedulerChannel, 0, schedulerInterface))
|
||||
def send_motd(send_message, MOTD, channel, interface):
|
||||
send_message(MOTD, channel, 0, interface)
|
||||
|
||||
# Send the MOTD every day at 13:00
|
||||
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00
|
||||
#logger.debug("System: Custom Scheduler: bbslink MeshBot looking for peers every other day at 10:00")
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
|
||||
def send_bbslink(send_message, channel, interface):
|
||||
send_message("bbslink MeshBot looking for peers", channel, 0, interface)
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Description=MeshingAround-ReportingTask
|
||||
|
||||
[Timer]
|
||||
OnUnitActiveSec=1h
|
||||
OnbootSec=5min
|
||||
OnCalendar=*-*-* 04:20:00
|
||||
Persistent=true
|
||||
Unit=mesh_bot_reporting.service
|
||||
#OnUnitActiveSec=1h
|
||||
#OnbootSec=5min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -14,6 +14,9 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 etc/report_generator5.py
|
||||
ExecStop=pkill -f report_generator5.py
|
||||
# ExecStart=python3 etc/report_generator.py
|
||||
# ExecStop=pkill -f report_generator.py
|
||||
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
@@ -23,4 +26,4 @@ Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
WantedBy=timers.target
|
||||
@@ -1,28 +0,0 @@
|
||||
# /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot_w3.service
|
||||
# sudo systemctl start mesh_bot_w3.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-W3Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
21
etc/mesh_bot_w3_server.tmp
Normal file
21
etc/mesh_bot_w3_server.tmp
Normal file
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=MeshingAround-WebServer
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# # Simulate meshing-around de K7MHI 2024
|
||||
from modules.log import * # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
|
||||
from modules.log import logger, getPrettyTime # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
|
||||
# Initialize the tool
|
||||
@@ -22,6 +23,8 @@ def get_name_from_number(nodeID, length='short', interface=1):
|
||||
# return random name for nodeID
|
||||
names = ["Max","Molly","Jake","Kelly"]
|
||||
return names[nodeID % len(names)]
|
||||
def mesh_bot(message, nodeID, deviceID):
|
||||
return "Meshing-Around Bot at your service!"
|
||||
#simulate GPS locations for testing
|
||||
locations = [
|
||||
(48.200909, -123.25719),
|
||||
|
||||
198
install.sh
198
install.sh
@@ -1,10 +1,71 @@
|
||||
#!/bin/bash
|
||||
# meshing-around install helper script
|
||||
# to uninstall, run with --nope
|
||||
|
||||
# install.sh
|
||||
NOPE=0
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--nope" ]]; then
|
||||
NOPE=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $NOPE -eq 1 ]]; then
|
||||
echo "Uninstalling Meshing Around and all related services..."
|
||||
|
||||
sudo systemctl stop mesh_bot || true
|
||||
sudo systemctl disable mesh_bot || true
|
||||
|
||||
sudo systemctl stop pong_bot || true
|
||||
sudo systemctl disable pong_bot || true
|
||||
|
||||
sudo systemctl stop mesh_bot_w3_server || true
|
||||
sudo systemctl disable mesh_bot_w3_server || true
|
||||
|
||||
sudo systemctl stop mesh_bot_reporting || true
|
||||
sudo systemctl disable mesh_bot_reporting || true
|
||||
|
||||
sudo rm -f /etc/systemd/system/mesh_bot.service
|
||||
sudo rm -f /etc/systemd/system/mesh_bot_reporting
|
||||
sudo rm -f /etc/systemd/system/pong_bot.service
|
||||
sudo rm -f /etc/systemd/system/mesh_bot_w3_server.service
|
||||
sudo rm -f /etc/systemd/system/mesh_bot_reporting.service
|
||||
sudo rm -f /etc/systemd/system/mesh_bot_reporting.timer
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl reset-failed
|
||||
|
||||
sudo gpasswd -d meshbot dialout || true
|
||||
sudo gpasswd -d meshbot tty || true
|
||||
sudo gpasswd -d meshbot bluetooth || true
|
||||
sudo groupdel meshbot || true
|
||||
sudo userdel meshbot || true
|
||||
|
||||
sudo rm -rf /opt/meshing-around/
|
||||
|
||||
# If Ollama was installed and you want to remove it:
|
||||
if [[ -f /etc/systemd/system/ollama.service ]]; then
|
||||
read -p "Ollama service detected. Do you want to remove Ollama and all its data? (y/n): " remove_ollama
|
||||
if [[ "$remove_ollama" =~ ^[Yy] ]]; then
|
||||
sudo systemctl stop ollama || true
|
||||
sudo systemctl disable ollama || true
|
||||
sudo rm -f /etc/systemd/system/ollama.service
|
||||
sudo rm -rf /usr/local/bin/ollama
|
||||
sudo rm -rf ~/.ollama
|
||||
echo "Ollama removed."
|
||||
else
|
||||
echo "Ollama not removed."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Uninstall complete. Hope to see you again! 73"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# install.sh, Meshing Around installer script
|
||||
# Thanks for using Meshing Around!
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "########################\n"
|
||||
@@ -76,17 +137,19 @@ else
|
||||
printf "\nDependencies installed\n"
|
||||
fi
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout "$USER"
|
||||
sudo usermod -a -G tty "$USER"
|
||||
sudo usermod -a -G bluetooth "$USER"
|
||||
|
||||
# copy service files
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
|
||||
cp etc/mesh_bot_w3_server.tmp etc/mesh_bot_w3_server.service
|
||||
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_w3_server.service
|
||||
|
||||
# copy modules/custom_scheduler.py template if it does not exist
|
||||
if [[ ! -f modules/custom_scheduler.py ]]; then
|
||||
@@ -184,15 +247,7 @@ else
|
||||
read bot
|
||||
fi
|
||||
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_w3.service
|
||||
# set the correct user in the service file?
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
# ask if we should add a user for the bot
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "\nDo you want to add a local user (meshbot) no login, for the bot? (y/n)"
|
||||
read meshbotservice
|
||||
@@ -208,7 +263,23 @@ if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" |
|
||||
else
|
||||
whoami=$(whoami)
|
||||
fi
|
||||
# set basic permissions for the bot user
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.timer
|
||||
# set the correct group in the service file
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.timer
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout "$whoami"
|
||||
sudo usermod -a -G tty "$whoami"
|
||||
sudo usermod -a -G bluetooth "$whoami"
|
||||
@@ -225,18 +296,6 @@ if ! systemctl is-active --quiet ntp.service && \
|
||||
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
|
||||
fi
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_w3.service
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
sed -i "$replace" etc/mesh_bot_reporting.service
|
||||
sed -i "$replace" etc/mesh_bot_w3.service
|
||||
printf "\n service files updated\n"
|
||||
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
# install service for pong bot
|
||||
@@ -256,6 +315,29 @@ if [[ $(echo "${bot}" | grep -i "^m") ]]; then
|
||||
service="mesh_bot"
|
||||
fi
|
||||
|
||||
# install mesh_bot_reporting timer to run daily at 4:20 am
|
||||
echo ""
|
||||
echo "Installing mesh_bot_reporting.timer to run mesh_bot_reporting daily at 4:20 am..."
|
||||
sudo cp etc/mesh_bot_reporting.service /etc/systemd/system/
|
||||
sudo cp etc/mesh_bot_reporting.timer /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable mesh_bot_reporting.timer
|
||||
sudo systemctl start mesh_bot_reporting.timer
|
||||
echo "mesh_bot_reporting.timer installed and enabled"
|
||||
echo "Check timer status with: systemctl status mesh_bot_reporting.timer"
|
||||
echo "List all timers with: systemctl list-timers"
|
||||
echo ""
|
||||
|
||||
# # install mesh_bot_w3_server service
|
||||
# echo "Installing mesh_bot_w3_server.service to run the web3 server..."
|
||||
# sudo cp etc/mesh_bot_w3_server.service /etc/systemd/system/
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot_w3_server.service
|
||||
# sudo systemctl start mesh_bot_w3_server.service
|
||||
# echo "mesh_bot_w3_server.service installed and enabled"
|
||||
# echo "Check service status with: systemctl status mesh_bot_w3_server.service"
|
||||
# echo ""
|
||||
|
||||
# check if running on embedded for final steps
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
# ask if emoji font should be installed for linux
|
||||
@@ -315,8 +397,14 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "\n older chron statment to run the report generator hourly:\n" >> install_notes.txt
|
||||
printf "0 * * * * /usr/bin/python3 $program_path/etc/report_generator5.py" >> install_notes.txt
|
||||
printf " to edit crontab run 'crontab -e'\n" >> install_notes.txt
|
||||
printf "\nmesh_bot_reporting.timer installed to run daily at 4:20 am\n" >> install_notes.txt
|
||||
printf "Check timer status: systemctl status mesh_bot_reporting.timer\n" >> install_notes.txt
|
||||
printf "List all timers: systemctl list-timers\n" >> install_notes.txt
|
||||
printf "View timer logs: journalctl -u mesh_bot_reporting.timer\n" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
@@ -347,14 +435,17 @@ else
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
# check if the cron job already exists
|
||||
if ! crontab -l | grep -q "$chronjob"; then
|
||||
# add the cron job to run the report_generator5.py script
|
||||
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
|
||||
printf "\nAdded cron job to run report_generator5.py\n"
|
||||
else
|
||||
printf "\nCron job already exists, skipping\n"
|
||||
fi
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
# # check if the cron job already exists
|
||||
# if ! crontab -l | grep -q "$chronjob"; then
|
||||
# # add the cron job to run the report_generator5.py script
|
||||
# (crontab -l 2>/dev/null; echo "$chronjob") | crontab -
|
||||
# printf "\nAdded cron job to run report_generator5.py\n"
|
||||
# else
|
||||
# printf "\nCron job already exists, skipping\n"
|
||||
# fi
|
||||
# document the service install
|
||||
printf "Reference following commands:\n\n" > install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
@@ -363,6 +454,12 @@ else
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "older crontab to run the report generator hourly:" >> install_notes.txt
|
||||
printf "0 * * * * /usr/bin/python3 $program_path/etc/report_generator5.py" >> install_notes.txt
|
||||
printf " to edit crontab run 'crontab -e'" >> install_notes.txt
|
||||
printf "\nmesh_bot_reporting.timer installed to run daily at 4:20 am\n" >> install_notes.txt
|
||||
printf "Check timer status: systemctl status mesh_bot_reporting.timer\n" >> install_notes.txt
|
||||
printf "List all timers: systemctl list-timers\n" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
@@ -374,13 +471,23 @@ exit 0
|
||||
|
||||
# sudo systemctl stop mesh_bot
|
||||
# sudo systemctl disable mesh_bot
|
||||
|
||||
# sudo systemctl stop pong_bot
|
||||
# sudo systemctl disable pong_bot
|
||||
|
||||
# sudo systemctl stop mesh_bot_w3_server
|
||||
# sudo systemctl disable mesh_bot_w3_server
|
||||
|
||||
# sudo systemctl stop mesh_bot_reporting
|
||||
# sudo systemctl disable mesh_bot_reporting
|
||||
|
||||
# sudo rm /etc/systemd/system/mesh_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting
|
||||
# sudo rm /etc/systemd/system/pong_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3_server.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting.timer
|
||||
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl reset-failed
|
||||
|
||||
@@ -390,7 +497,14 @@ exit 0
|
||||
# sudo groupdel meshbot
|
||||
# sudo userdel meshbot
|
||||
|
||||
# sudo rm -rf /opt/meshing-around
|
||||
# sudo rm -rf /opt/meshing-around/
|
||||
|
||||
# If Ollama was installed and you want to remove it:
|
||||
# sudo systemctl stop ollama
|
||||
# sudo systemctl disable ollama
|
||||
# sudo rm /etc/systemd/system/ollama.service
|
||||
# sudo rm -rf /usr/local/bin/ollama
|
||||
# sudo rm -rf ~/.ollama
|
||||
|
||||
|
||||
# after install shenannigans
|
||||
|
||||
559
mesh_bot.py
559
mesh_bot.py
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,8 @@
|
||||
# Meshtastic Mesh-Bot Modules
|
||||
|
||||
This document provides an overview of all modules available in the Mesh-Bot project, including their features, usage, and configuration.
|
||||
Updated Oct-2025 "ver 1.9.8.4"
|
||||
This document provides an overview of all modules available in the Mesh-Bot project, including their features, usage, and configuration.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
@@ -13,19 +11,19 @@ Updated Oct-2025 "ver 1.9.8.4"
|
||||
- [BBS (Bulletin Board System)](#bbs-bulletin-board-system)
|
||||
- [Checklist](#checklist)
|
||||
- [Location & Weather](#location--weather)
|
||||
- [Map Command](#map-command)
|
||||
- [EAS & Emergency Alerts](#eas--emergency-alerts)
|
||||
- [File Monitoring & News](#file-monitoring--news)
|
||||
- [Radio Monitoring](#radio-monitoring)
|
||||
- [Voice Commands (VOX)](#voice-commands-vox)
|
||||
- [Ollama LLM/AI](#ollama-llmai)
|
||||
- [Wikipedia Search](#wikipedia-search)
|
||||
- [Scheduler](#-mesh-bot-scheduler-user-guide)
|
||||
- [DX Spotter Module](#dx-spotter-module)
|
||||
- [Mesh Bot Scheduler User Guide](#mesh-bot-scheduler-user-guide)
|
||||
- [Other Utilities](#other-utilities)
|
||||
- [Configuration](#configuration)
|
||||
- [Messaging Settings](#messaging-settings)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Adding your Own](adding_more.md)
|
||||
- [Configuration Guide](#configuration-guide)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -262,12 +260,70 @@ Configure in `[ollama]` section of `config.ini`.
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `wiki:` | Search Wikipedia or local Kiwix server |
|
||||
| `wiki` | Search Wikipedia or local Kiwix server |
|
||||
|
||||
Configure in `[wikipedia]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## DX Spotter Module
|
||||
|
||||
The DX Spotter module allows you to fetch and display recent DX cluster spots from [spothole.app](https://spothole.app) directly in your mesh-bot.
|
||||
|
||||
### Command
|
||||
|
||||
| Command | Description |
|
||||
|---------|------------------------------|
|
||||
| `dx` | Show recent DX cluster spots |
|
||||
|
||||
###Usage
|
||||
|
||||
Send a message to the bot containing the `dx` command. You can add filters to narrow down the results:
|
||||
|
||||
- **Basic usage:**
|
||||
```
|
||||
dx
|
||||
```
|
||||
Returns the latest DX spots.
|
||||
|
||||
- **With filters:**
|
||||
```
|
||||
dx band=20m mode=SSB
|
||||
dx xota=WWFF
|
||||
dx by=K7MHI
|
||||
```
|
||||
- `band=`: Filter by band (e.g., 20m, 40m)
|
||||
- `mode=`: Filter by mode (e.g., SSB, CW, FT8)
|
||||
- `ota=`: Filter by source/group (e.g., WWFF, POTA, SOTA)
|
||||
- `of=`: Filter by callsign of the spotted DX
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
K7ABC @14.074 MHz FT8 WWFF KFF-1234 by:N0CALL CN87 Some comment
|
||||
W1XYZ @7.030 MHz CW SOTA W7W/WE-001 by:K7MHI CN88
|
||||
```
|
||||
|
||||
- Each line shows:
|
||||
`DX_CALL @FREQUENCY MODE GROUP GROUP_REF by:SPOTTER_CALL SPOTTER_GRID COMMENT`
|
||||
|
||||
### Notes
|
||||
|
||||
- Returns up to 4 of the most recent spots matching your filters.
|
||||
- Data is fetched from [spothole.app](https://spothole.app/).
|
||||
- If no spots are found, you’ll see:
|
||||
`No DX spots found.`
|
||||
|
||||
### Configuration
|
||||
```ini
|
||||
[radioMon]
|
||||
dxspotter_enabled = True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 📅 Mesh Bot Scheduler User Guide
|
||||
|
||||
Automate messages and tasks using the scheduler module.
|
||||
@@ -285,9 +341,9 @@ See modules/custom_scheduler.py for advanced scheduling using python
|
||||
|
||||
**Features:**
|
||||
- **Basic Scheduling:** Send messages on a set schedule (e.g., every day at 09:00, every Monday at noon, every hour, etc.).
|
||||
- **Joke Scheduler:** Automatically send jokes at a chosen interval.
|
||||
- **Weather Scheduler:** Send weather updates at a chosen interval.
|
||||
- **Custom Scheduler:** Import and run your own scheduled jobs by editing `custom_scheduler.py`.
|
||||
- **Joke Scheduler:** Automatically send jokes every x min
|
||||
- **Weather Scheduler:** Send weather updates at time of day, daily.
|
||||
- **Custom Scheduler:** run your own scheduled jobs by editing `custom_scheduler.py`.
|
||||
- **Logging:** All scheduling actions are logged for debugging and monitoring.
|
||||
|
||||
**Example Configuration:**
|
||||
@@ -306,7 +362,6 @@ To send a daily message at 09:00:
|
||||
- All scheduled jobs run asynchronously as long as the bot is running.
|
||||
- For troubleshooting, check the logs for scheduler activity and errors.
|
||||
|
||||
|
||||
### Basic Scheduler Options
|
||||
|
||||
You can schedule messages or actions using the following options in your configuration:
|
||||
@@ -348,11 +403,11 @@ You can schedule messages or actions using the following options in your configu
|
||||
- → Sends a bbslink message every 2 hours.
|
||||
|
||||
#### **weather**
|
||||
- Schedules the bot to send a weather update at the specified interval (in hours).
|
||||
- Schedules the bot to send a weather update at the specified time of day, daily.
|
||||
- **Example:**
|
||||
- Option: `weather`
|
||||
- Interval: `3`
|
||||
- → Sends a weather update every 3 hours.
|
||||
- Time: `08:00`
|
||||
- → Sends a weather update daily at 8:00a.
|
||||
|
||||
---
|
||||
|
||||
@@ -368,16 +423,6 @@ You can use any of these options to schedule messages on specific days:
|
||||
|
||||
---
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
- **schedulerValue**: The schedule type (e.g., `day`, `joke`, `weather`, `mon`, etc.)
|
||||
- **schedulerTime**: The time to run (e.g., `08:00`). Leave blank for interval-based schedules.
|
||||
- **schedulerInterval**: The interval (e.g., `2` for every 2 hours/days/minutes).
|
||||
- **schedulerChannel**: The channel number to send to.
|
||||
- **schedulerInterface**: The device/interface number.
|
||||
|
||||
---
|
||||
|
||||
## Other Utilities
|
||||
|
||||
- `motd` — Message of the day
|
||||
@@ -538,9 +583,6 @@ If you continue to have issues, review the logs for error messages and consult t
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
@@ -840,39 +882,5 @@ qrz_hello_string = "send CMD or DM me for more info." # will be sent to all hear
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
#Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
```
|
||||
```ini
|
||||
bbslink_enabled = True
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
|
||||
Happy meshing!
|
||||
@@ -129,4 +129,79 @@ This will call the default script located at `script/runShell.sh` and return its
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Overview Unit Tests
|
||||
|
||||
Your test_bot.py file contains a comprehensive suite of unit tests for the various modules the project. The tests are organized using Python’s `unittest` framework and cover both core utility modules and all major game modules.
|
||||
|
||||
---
|
||||
|
||||
## Structure
|
||||
|
||||
- **Imports & Setup:**
|
||||
The script sets up the environment, imports all necessary modules, and suppresses certain warnings for clean test output.
|
||||
|
||||
- **TestBot Class:**
|
||||
All tests are methods of the `TestBot` class, which inherits from `unittest.TestCase`.
|
||||
|
||||
---
|
||||
|
||||
## Core Module Tests
|
||||
|
||||
- **Database & Checklist:**
|
||||
- `test_load_bbsdb`, `test_bbs_list_messages`, `test_initialize_checklist_database`
|
||||
- **News & Alerts:**
|
||||
- `test_init_news_sources`, `test_get_nina_alerts`
|
||||
- **LLM & Wikipedia:**
|
||||
- `test_llmTool_get_google`, `test_send_ollama_query`, `test_get_wikipedia_summary`, `test_get_kiwix_summary`
|
||||
- **Space & Weather:**
|
||||
- `test_get_moon_phase`, `test_get_sun_times`, `test_hf_band_conditions`
|
||||
- **Radio & Location:**
|
||||
- `test_get_hamlib`, `test_get_rss_feed`, `get_openskynetwork`, `test_initalize_qrz_database`
|
||||
|
||||
---
|
||||
|
||||
## Game Module Tests
|
||||
|
||||
Each game module has a dedicated test that simulates a typical user interaction:
|
||||
|
||||
- **Tic-Tac-Toe:**
|
||||
Starts a game and makes one move.
|
||||
- **Video Poker:**
|
||||
Starts a session and places a bet.
|
||||
- **Blackjack:**
|
||||
Starts a game and places a bet.
|
||||
- **Hangman:**
|
||||
Starts a game and guesses a letter.
|
||||
- **Lemonade Stand:**
|
||||
Starts a game and buys a box of cups.
|
||||
- **GolfSim:**
|
||||
Starts a hole and takes a shot.
|
||||
- **DopeWars:**
|
||||
Starts a game, selects a city, and checks the list.
|
||||
- **MasterMind:**
|
||||
Starts a game and makes one guess.
|
||||
- **Quiz:**
|
||||
Starts a quiz, joins as a player, answers one question, and ends the quiz.
|
||||
- **Survey:**
|
||||
Starts a survey, answers one question, and ends the survey.
|
||||
- **HamTest:**
|
||||
Starts a ham radio test and answers one question.
|
||||
|
||||
---
|
||||
|
||||
## Extended API Tests
|
||||
|
||||
If the `.checkall` file is present, additional API and data-fetching tests are run for:
|
||||
- RepeaterBook, ArtSciRepeaters, NOAA tides/weather, USGS earthquakes/volcanoes, satellite passes, and more.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tests are designed to be **non-destructive** and **idempotent**.
|
||||
- Some tests require specific data files (e.g., for quiz, survey, hamtest).
|
||||
- The suite is intended to be run from the main program directory.
|
||||
|
||||
|
||||
|
||||
|
||||
Happy hacking!
|
||||
@@ -8,50 +8,56 @@ This document covers the Bulliten Board System or BBS componment of the meshing-
|
||||
## Table of Contents
|
||||
|
||||
1. [BBS Core Functions](#1-bbs-core-functions)
|
||||
- [Direct Messages (DMs)](#11-direct-messages-dms)
|
||||
2. [BBS Database Sync: File-Based (Out-of-Band)](#1-bbs-database-sync-file-based-out-of-band)
|
||||
3. [BBS Over-the-Air (OTA) Sync: Linking](#2-bbs-over-the-air-ota-sync-linking)
|
||||
4. [Scheduling BBS Sync](#3-scheduling-bbs-sync)
|
||||
5. [Best Practices](#4-best-practices)
|
||||
6. [Example: Full Sync Workflow](#5-example-full-sync-workflow)
|
||||
7. [Troubleshooting](#6-troubleshooting)
|
||||
8. [API Reference: BBS Sync](#7-api-reference-bbs-sync)
|
||||
- [Central Message Store](#11-central-message-store)
|
||||
- [Direct Mail (DM) Messages](#12-direct-mail-dm-messages)
|
||||
- [BBS Commands](#bbs-commands)
|
||||
2. [Synchronization bot2bot: Full Sync Workflow](#2-synchronization-bot2bot--full-sync-workflow)
|
||||
- [BBS Database Sync: File-Based (Out-of-Band)](#21-bbs-database-sync-file-based-out-of-band)
|
||||
- [BBS Over-the-Air (OTA) Sync: Linking](#22-bbs-over-the-air-ota-sync-linking)
|
||||
- [Scheduling BBS Auto Sync](#23-scheduling-bbs-auto-sync)
|
||||
3. [Troubleshooting](#4-troubleshooting)
|
||||
4. [API Reference: BBS Sync](#5-api-reference-bbs-sync)
|
||||
5. [Best Practices](#5-best-practices)
|
||||
|
||||
## 1. **BBS Core Functions**
|
||||
The mesh-bot provides a basic message mail system for Meshtastic
|
||||
|
||||
## 1.1 **Direct Messages (DMs)**
|
||||
## 1.1 Central Message Store
|
||||
|
||||
### **How DMs Work**
|
||||
- Direct Messages (DMs) are private messages sent from one node to another.
|
||||
- DMs are stored separately from public posts in `data/bbsdm.pkl`.
|
||||
- Each DM entry in the pickle, typically includes: `[id, toNode, message, fromNode, timestamp, threadID, replytoID]`.
|
||||
- **Shared public message space** for all nodes.
|
||||
- Classic BBS list with a simple, one-level message tree.
|
||||
- Messages are stored in `data/bbsdb.pkl`.
|
||||
- Each entry typically includes:
|
||||
`[id, subject, body, fromNode, timestamp, threadID, replytoID]`
|
||||
|
||||
### **DM Delivery**
|
||||
- When a DM is posted using `bbs_post_dm(toNode, message, fromNode)`, it is added to the recipient's DM database.
|
||||
- DMs can be delivered in two ways:
|
||||
1. **File-Based Sync:**
|
||||
- The `bbsdm.pkl` file is copied between nodes using SCP, rsync, or other file transfer methods.
|
||||
- After syncing, the recipient node can check for new DMs using `bbs_check_dm(toNode)`.
|
||||
2. **Over-the-Air (OTA) Sync:**
|
||||
- DMs can be exchanged between nodes using the same OTA sync mechanism as other posts.
|
||||
- The bot will receive (onRX) or detect any packet and deliver the DM/mail to the recipient.
|
||||
- DMs are only visible to the intended recipient node and are not listed in the public message list.
|
||||
### Posting to Public
|
||||
|
||||
### **DM Commands**
|
||||
| Command | Description |
|
||||
|-----------------|---------------------------------------------|
|
||||
| `bbs_post_dm` | Send a direct message to another node |
|
||||
| `bbs_check_dm` | Check for new DMs for your node |
|
||||
| `bbs_delete_dm` | Delete a DM after reading |
|
||||
To post a public message:
|
||||
```sh
|
||||
bbspost $Subject #Message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Direct Mail (DM) Messages
|
||||
- **DMs are private messages** sent from one node to another.
|
||||
- Stored separately from public posts in `data/bbsdm.pkl`.
|
||||
- Each DM entry typically includes:
|
||||
`[id, toNode, message, fromNode, timestamp, threadID, replytoID]`
|
||||
- You can inject DMs directly for automation using the `script/injectDM.py` tool.
|
||||
|
||||
### **Message Storage**
|
||||
The .. database is
|
||||
- Messages are stored in `data/bbsdb.pkl` (public posts) and `data/bbsdm.pkl` (direct messages).
|
||||
- Format: Each message is a list, e.g. `[id, subject, body, fromNode, timestamp, threadID, replytoID]`.
|
||||
### DM Delivery
|
||||
|
||||
- To post a DM, use:
|
||||
```sh
|
||||
bbspost @USER #Message
|
||||
```
|
||||
- When a DM is posted, it is added to the DM database.
|
||||
- When the bot detects the recipient node on the network, it delivers the DM and then removes it from local storage.
|
||||
|
||||
---
|
||||
|
||||
### BBS Commands
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
@@ -64,12 +70,26 @@ The .. database is
|
||||
| `bbslink` | Link messages between BBS systems |
|
||||
|
||||
---
|
||||
Enable in `[bbs]` section of `config.ini`.
|
||||
|
||||
## 1. **BBS Database Sync: File-Based (Out-of-Band)**
|
||||
## 2. **Synchronization bot2bot : Full Sync Workflow**
|
||||
|
||||
1. **Set up a dedicated sync channel** (e.g., channel bot-admin).
|
||||
2. **Configure both nodes** with `bbs_link_enabled = True` and add each other to `bbs_link_whitelist`.
|
||||
3. **Schedule sync** every hour:
|
||||
- Node A sends `bbslink 0` to Node B on channel 99.
|
||||
- Node B responds with messages and `bbsack`.
|
||||
4. **Optionally, use SSH/scp** to copy `bbsdb.pkl` for full out-of-band backup.
|
||||
|
||||
|
||||
## 2.1. **BBS Database Sync: File-Based (Out-of-Band)**
|
||||
|
||||
### **Manual/Automated File Sync (e.g., SSH/SCP)**
|
||||
- **Purpose:** Sync BBS data between nodes by copying `bbsdb.pkl` and `bbsdm.pkl` files.
|
||||
```ini
|
||||
[bbs]
|
||||
# The "api" needs enabled which enables file polling
|
||||
bbsAPI_enabled = True
|
||||
```
|
||||
- **How-To:**
|
||||
1. **Locate Files:**
|
||||
- `data/bbsdb.pkl` (public posts)
|
||||
@@ -88,7 +108,7 @@ Enable in `[bbs]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## 2. **BBS Over-the-Air (OTA) Sync: Linking**
|
||||
## 2.2. **BBS Over-the-Air (OTA) Sync: Linking**
|
||||
### **How OTA Sync Works**
|
||||
- Nodes can exchange BBS messages using special commands over the mesh network.
|
||||
- Uses `bbslink` and `bbsack` commands for message exchange.
|
||||
@@ -109,36 +129,43 @@ Enable in `[bbs]` section of `config.ini`.
|
||||
- For high-reliability sync, configure a dedicated channel (not used for chat).
|
||||
---
|
||||
|
||||
## 3. **Scheduling BBS Sync**
|
||||
## 2.3. **Scheduling BBS Auto Sync**
|
||||
|
||||
### **Using the Bot’s Scheduler**
|
||||
|
||||
- You can schedule periodic sync requests to a peer node.
|
||||
- Example: Every hour, send a `bbslink` request to a peer.
|
||||
see more at [Module Readme](README.md#scheduler)
|
||||
---
|
||||
|
||||
## 4. **Best Practices**
|
||||
|
||||
- **Backup:** Regularly back up `bbsdb.pkl` and `bbsdm.pkl`.
|
||||
- **Security:** Use SSH keys for file transfer; restrict OTA sync to trusted nodes.
|
||||
- **Reliability:** Use a dedicated channel for BBS sync to avoid chat congestion.
|
||||
- **Automation:** Use the scheduler for regular syncs, both file-based and OTA.
|
||||
|
||||
---
|
||||
|
||||
## 5. **Example: Full Sync Workflow**
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
|
||||
|
||||
1. **Set up a dedicated sync channel** (e.g., channel bot-admin).
|
||||
2. **Configure both nodes** with `bbs_link_enabled = True` and add each other to `bbs_link_whitelist`.
|
||||
3. **Schedule sync** every hour:
|
||||
- Node A sends `bbslink 0` to Node B on channel 99.
|
||||
- Node B responds with messages and `bbsack`.
|
||||
4. **Optionally, use SSH/scp** to copy `bbsdb.pkl` for full out-of-band backup.
|
||||
```ini
|
||||
[bbs]
|
||||
bbslink_enabled = True
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
|
||||
[scheduler]
|
||||
enabled = True
|
||||
interface = 1
|
||||
channel = 2
|
||||
value = link
|
||||
interval = 12 # 12 hours
|
||||
```
|
||||
|
||||
```python
|
||||
# Custom Schedule Example if using custom for [scheduler]
|
||||
# Send bbslink looking for peers every 2 days at 10 AM
|
||||
schedule.every(2).days.at("10:00").do(send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. **Troubleshooting**
|
||||
---
|
||||
|
||||
## 4. **Troubleshooting**
|
||||
|
||||
- **Messages not syncing?**
|
||||
- Check `bbs_link_enabled` and whitelist settings.
|
||||
@@ -149,7 +176,19 @@ see more at [Module Readme](README.md#scheduler)
|
||||
- Verify file permissions and paths.
|
||||
- Ensure the bot reloads the database after file copy.
|
||||
|
||||
## 7. **API Reference: BBS Sync**
|
||||
- **Custom file problems?**
|
||||
- remove the custom_scheduler.py and replace it with [etc/custom_scheduler.py](etc/custom_scheduler.py)
|
||||
|
||||
The bbs link command should include `bbslink`
|
||||
`.do(send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))`
|
||||
|
||||
```ini
|
||||
[bbs]
|
||||
# The "api" needs enabled which enables file polling and use of `script/injectDM.py`
|
||||
bbsAPI_enabled = True
|
||||
```
|
||||
|
||||
## 5. **API Reference: BBS Sync**
|
||||
|
||||
### **Key Functions in Python**
|
||||
| Function | Purpose | Usage Example |
|
||||
@@ -185,5 +224,11 @@ Future Use
|
||||
- Receiving node uses `bbs_receive_compressed()`.
|
||||
|
||||
---
|
||||
### 5. **Best Practices**
|
||||
|
||||
- **Backup:** Regularly back up `bbsdb.pkl` and `bbsdm.pkl`.
|
||||
- **Security:** Use SSH keys for file transfer; restrict OTA sync to trusted nodes.
|
||||
- **Reliability:** Use a dedicated channel for BBS sync to avoid chat congestion.
|
||||
- **Automation:** Use the scheduler for regular syncs, both file-based and OTA.
|
||||
|
||||
---
|
||||
@@ -2,7 +2,8 @@
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import bbs_admin_list, bbs_ban_list, MESSAGE_CHUNK_SIZE, bbs_link_enabled, bbs_link_whitelist, responseDelay
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
@@ -21,30 +22,32 @@ bbs_dm = []
|
||||
|
||||
def load_bbsdb():
|
||||
global bbs_messages
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'rb') as f:
|
||||
new_bbs_messages = pickle.load(f)
|
||||
if isinstance(new_bbs_messages, list):
|
||||
for msg in new_bbs_messages:
|
||||
#example [1, 'Welcome to meshBBS', 'Welcome to the BBS, please post a message!', 0]
|
||||
msgHash = hash(tuple(msg[1:3])) # Create a hash of the message content (subject and body)
|
||||
# Check if the message already exists in bbs_messages
|
||||
msgHash = hash(tuple(msg[1:3]))
|
||||
if all(hash(tuple(existing_msg[1:3])) != msgHash for existing_msg in bbs_messages):
|
||||
# if the message is not a duplicate, add it to bbs_messages Maintain the message ID sequence
|
||||
new_id = len(bbs_messages) + 1
|
||||
bbs_messages.append([new_id, msg[1], msg[2], msg[3]])
|
||||
return True # Loaded successfully, regardless of whether new messages were added
|
||||
return False # File existed but did not contain a valid list of messages (possibly corrupted)
|
||||
except FileNotFoundError:
|
||||
# create a new bbsdb.pkl with a welcome message
|
||||
# template ([messageID, subject, message, fromNode, now, thread, replyto])
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0,time.strftime('%Y-%m-%d %H:%M:%S'),0,0]]
|
||||
logger.debug("System: bbsdb.pkl not found, creating new one")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error creating bbsdb.pkl: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error loading bbsdb.pkl: {e}")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
return False
|
||||
|
||||
def save_bbsdb():
|
||||
global bbs_messages
|
||||
|
||||
@@ -2,24 +2,28 @@
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import checklist_db, reverse_in_out, bbs_ban_list
|
||||
import time
|
||||
|
||||
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout")
|
||||
|
||||
def initialize_checklist_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
# Check if the checkin table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkin
|
||||
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
|
||||
# Check if the checkout table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkout
|
||||
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.debug("System: Ensured data/checklist.db exists with required tables")
|
||||
try:
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
# Check if the checkin table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkin
|
||||
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
|
||||
# Check if the checkout table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkout
|
||||
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Checklist: Failed to initialize database: {e}")
|
||||
return False
|
||||
|
||||
def checkin(name, date, time, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
|
||||
202
modules/dxspot.py
Normal file
202
modules/dxspot.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# meshing-around modules/dxspot.py - Handles DX Spotter integration
|
||||
# Fetches DX spots from Spothole API based on user commands
|
||||
# 2025 K7MHI Kelly Keeton
|
||||
import requests
|
||||
import datetime
|
||||
from modules.log import logger
|
||||
from modules.settings import latitudeValue, longitudeValue
|
||||
|
||||
trap_list_dxspotter = ["dx"]
|
||||
|
||||
def handledxcluster(message, nodeID, deviceID):
|
||||
from modules.dxspot import get_spothole_spots
|
||||
if "DX" in message.upper():
|
||||
logger.debug(f"System: DXSpotter: Device:{deviceID} Handler: DX Spot Request Received from Node {nodeID}")
|
||||
band = None
|
||||
mode = None
|
||||
source = None
|
||||
dx_call = None
|
||||
parts = message.split()
|
||||
for part in parts:
|
||||
if part.lower().startswith("band="):
|
||||
band = part.split("=")[1]
|
||||
elif part.lower().startswith("mode="):
|
||||
mode = part.split("=")[1]
|
||||
elif part.lower().startswith("ota="):
|
||||
source = part.split("=")[1]
|
||||
elif part.lower().startswith("of="):
|
||||
dx_call = part.split("=")[1]
|
||||
# Build params dict
|
||||
params = {}
|
||||
if source:
|
||||
params["source"] = source.upper()
|
||||
if band:
|
||||
params["band"] = band.lower()
|
||||
if mode:
|
||||
params["mode"] = mode.upper()
|
||||
if dx_call:
|
||||
params["dx_call"] = dx_call.upper()
|
||||
|
||||
# Fetch spots
|
||||
spots = get_spothole_spots(**params)
|
||||
if spots:
|
||||
response_lines = []
|
||||
for spot in spots[:5]:
|
||||
callsign = spot.get('dx_call', spot.get('callsign', 'N/A'))
|
||||
freq_hz = spot.get('freq', spot.get('frequency', None))
|
||||
frequency = f"{float(freq_hz)/1e6:.3f} MHz" if freq_hz else "N/A"
|
||||
mode_val = spot.get('mode', 'N/A')
|
||||
comment = spot.get('comment', '')
|
||||
if len(comment) > 111: # Truncate comment to 111 chars
|
||||
comment = comment[:111] + '...'
|
||||
sig = spot.get('sig', '')
|
||||
de_grid = spot.get('de_grid', '')
|
||||
de_call = spot.get('de_call', '')
|
||||
sig_ref_name = spot.get('sig_refs_names', [''])[0] if spot.get('sig_refs_names') else ''
|
||||
line = f"{callsign} @{frequency} {mode_val} {sig} {sig_ref_name} by:{de_call} {de_grid} {comment}"
|
||||
response_lines.append(line)
|
||||
response = "\n".join(response_lines)
|
||||
else:
|
||||
response = "No DX spots found."
|
||||
return response
|
||||
return "Error: No DX command found."
|
||||
|
||||
def get_spothole_spots(source=None, band=None, mode=None, date=None, dx_call=None, de_continent=None, de_location=None):
|
||||
"""
|
||||
Fetches spots from https://spothole.app/api/v1/spots with optional filters.
|
||||
Returns a list of spot dicts.
|
||||
"""
|
||||
url = "https://spothole.app/api/v1/spots"
|
||||
params = {}
|
||||
fetched_count = 0
|
||||
|
||||
|
||||
# Add administrative filters if provided
|
||||
qrt = False # Always fetch active spots
|
||||
needs_sig = False # Always need spots wth a group ike Xota
|
||||
limit = 4
|
||||
dedupe = True
|
||||
|
||||
params["dedupe"] = str(dedupe).lower()
|
||||
params["limit"] = limit
|
||||
params["qrt"] = str(qrt).lower()
|
||||
params["needs_sig"] = str(needs_sig).lower()
|
||||
params["needs_sig_ref"] = 'true'
|
||||
# Only get spots from last 9 hours
|
||||
received_since_dt = datetime.datetime.utcnow() - datetime.timedelta(hours=9)
|
||||
received_since = int(received_since_dt.timestamp())
|
||||
params["received_since"] = received_since
|
||||
|
||||
# Add spot filters if provided
|
||||
if source:
|
||||
params["source"] = source
|
||||
if band:
|
||||
params["band"] = band
|
||||
if mode:
|
||||
params["mode"] = mode
|
||||
if date:
|
||||
# date should be a string in YYYY-MM-DD or datetime.date
|
||||
if isinstance(date, datetime.date):
|
||||
params["date"] = date.isoformat()
|
||||
else:
|
||||
params["date"] = date
|
||||
|
||||
try:
|
||||
headers = {"User-Agent": "meshing-around-dxspotter/1.0"}
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
spots = response.json()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error fetching spots: {e}")
|
||||
spots = []
|
||||
|
||||
fetched_count = len(spots)
|
||||
|
||||
# Admin Filters done via config.ini
|
||||
de_grid = None # e.g., "EM00"
|
||||
de_dxcc_id = None # e.g., "291"
|
||||
de_call = None # e.g., "K7MHI"
|
||||
|
||||
dx_itu_zone = None # e.g., "3"
|
||||
dx_cq_zone = None # e.g., "4"
|
||||
dx_dxcc_id = None # e.g., "291"
|
||||
|
||||
# spotter filters
|
||||
|
||||
# location filter
|
||||
de_latitude = None # e.g., 34.05
|
||||
de_longitude = None # e.g., -118.25
|
||||
if de_location:
|
||||
de_latitude, de_longitude = de_location
|
||||
elif de_latitude is not None and de_longitude is not None:
|
||||
de_latitude = latitudeValue
|
||||
de_longitude = longitudeValue
|
||||
if de_latitude and de_longitude:
|
||||
lat_range = (de_latitude - 1.0, de_latitude + 1.0)
|
||||
lon_range = (de_longitude - 1.0, de_longitude + 1.0)
|
||||
spots = [spot for spot in spots if lat_range[0] <= spot.get('de_latitude', 0) <= lat_range[1] and
|
||||
lon_range[0] <= spot.get('de_longitude', 0) <= lon_range[1]]
|
||||
# grid filter
|
||||
if de_grid:
|
||||
spots = [spot for spot in spots if spot.get('de_grid', '').upper() == de_grid.upper()]
|
||||
# DXCC Filters
|
||||
if de_dxcc_id:
|
||||
spots = [spot for spot in spots if str(spot.get('de_dxcc_id', '')) == str(de_dxcc_id)]
|
||||
# By reporting callsign
|
||||
if de_call:
|
||||
spots = [spot for spot in spots if spot.get('de_call', '').upper() == de_call.upper()]
|
||||
# DX spotted in zone
|
||||
if dx_itu_zone:
|
||||
spots = [spot for spot in spots if str(spot.get('dx_itu_zone', '')) == str(dx_itu_zone)]
|
||||
if dx_cq_zone:
|
||||
spots = [spot for spot in spots if str(spot.get('dx_cq_zone', '')) == str(dx_cq_zone)]
|
||||
if dx_dxcc_id:
|
||||
spots = [spot for spot in spots if str(spot.get('dx_dxcc_id', '')) == str(dx_dxcc_id)]
|
||||
|
||||
# User Runtime Filters
|
||||
|
||||
# Filter by dx_call if provided
|
||||
if dx_call:
|
||||
spots = [spot for spot in spots if spot.get('dx_call', '').upper() == dx_call.upper()]
|
||||
|
||||
# Filter by de_continent if provided
|
||||
if de_continent:
|
||||
spots = [spot for spot in spots if spot.get('de_continent', '').upper() == de_continent.upper()]
|
||||
|
||||
# Filter by de_location if provided
|
||||
if de_location:
|
||||
spots = [spot for spot in spots if spot.get('de_location', '').upper() == de_location.upper()]
|
||||
logger.debug(f"System: Spothole Returning {len(spots)} spots after filtering (fetched {fetched_count})")
|
||||
return spots
|
||||
|
||||
def handle_post_dxspot():
|
||||
time = int(datetime.datetime.utcnow().timestamp())
|
||||
freq = 14200000 # 14 MHz
|
||||
comment = "Test spot please ignore"
|
||||
de_spot = "N0CALL"
|
||||
dx_spot = "N0CALL"
|
||||
spot = {"dx_call": dx_spot, "time": time, "freq": freq, "comment": comment, "de_call": de_spot}
|
||||
try:
|
||||
success = post_spothole_spot(spot)
|
||||
if success:
|
||||
return "Spot posted successfully."
|
||||
else:
|
||||
return "Failed to post spot."
|
||||
except Exception as e:
|
||||
logger.debug(f"Error in handle_post_dxspot: {e}")
|
||||
return "Error occurred while posting spot."
|
||||
|
||||
def post_spothole_spot(spot):
|
||||
"""
|
||||
Posts a new spot to https://spothole.app/api/v1/spot.
|
||||
"""
|
||||
url = "https://spothole.app/api/v1/spot"
|
||||
headers = {"Content-Type": "application/json", "User-Agent": "meshing-around-dxspotter/1.0"}
|
||||
try:
|
||||
response = requests.post(url, json=spot, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
logger.debug(f"Spot posted successfully: {response.json()}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error posting spot: {e}")
|
||||
return False
|
||||
@@ -1,7 +1,17 @@
|
||||
# File monitor module for the meshing-around bot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import (
|
||||
file_monitor_file_path,
|
||||
news_file_path,
|
||||
news_random_line_only,
|
||||
allowXcmd,
|
||||
bbs_admin_list,
|
||||
xCmd2factorEnabled,
|
||||
xCmd2factor_timeout,
|
||||
enable_runShellCmd
|
||||
)
|
||||
import asyncio
|
||||
import random
|
||||
import os
|
||||
@@ -178,6 +188,9 @@ def initNewsSources():
|
||||
if file.endswith('_news.txt'):
|
||||
source = file[:-9] # remove _news.txt
|
||||
newsSourcesList.append(source)
|
||||
return True
|
||||
logger.info("FileMon: No news sources found")
|
||||
return False
|
||||
|
||||
#initialize the headlines on startup
|
||||
initNewsSources()
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
- [Tic-Tac-Toe](#tic-tac-toe-game-module)
|
||||
- [MasterMind](#mastermind-game-module)
|
||||
- [Video Poker](#video-poker-game-module)
|
||||
- [Hangman](#hangman-game-module)
|
||||
- [Quiz](#quiz-game-module)
|
||||
- [Survey](#survey--module-game)
|
||||
- [Word of the Day Game](#word-of-the-day-game--rules--features)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -538,4 +540,182 @@ Place your Bet, or (L)eave Table.
|
||||
"turtle",
|
||||
"lizard",
|
||||
"snake"
|
||||
]
|
||||
]
|
||||
|
||||
# Hangman Game Module
|
||||
|
||||
A classic word-guessing game for the Meshtastic mesh-bot. Try to guess the hidden word one letter at a time before you run out of chances!
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `hangman` via DM to the bot to begin a new game.
|
||||
|
||||
- **Objective:**
|
||||
Guess the secret word by suggesting letters, one at a time. Each incorrect guess brings you closer to losing!
|
||||
|
||||
- **Game Flow:**
|
||||
1. **New Game:**
|
||||
- The bot picks a random word and shows you its masked form (e.g., `_ _ _ _ _`).
|
||||
- You’ll see your total games played and games won.
|
||||
2. **Guessing:**
|
||||
- Type a single letter to guess.
|
||||
- Correct guesses reveal all instances of that letter in the word.
|
||||
- Incorrect guesses are tracked; you have 6 chances before the game ends.
|
||||
- The bot shows your progress, wrong guesses, and a hangman emoji status.
|
||||
3. **Winning & Losing:**
|
||||
- Guess all letters before reaching 6 wrong guesses to win!
|
||||
- If you lose, the bot reveals the word and starts a new game.
|
||||
|
||||
- **Commands:**
|
||||
- Enter a single letter to guess.
|
||||
- Start a new game by sending `hangman` again.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
_ _ _ _ _ _ _
|
||||
Guess a letter
|
||||
|
||||
|
||||
🥳
|
||||
Total Games: 1, Won: 1
|
||||
M E S H T A S T I C
|
||||
Guess a letter
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The word list is loaded from `data/hangman.json` if available, or uses a built-in default list. [\"apple\",\"banana\",\"cherry\"]
|
||||
- Game stats are tracked per player.
|
||||
- Only one game session per player at a time.
|
||||
- Play via DM for best experience.
|
||||
|
||||
## Data Files
|
||||
|
||||
- `data/hangman.json`: List of words for Hangman.
|
||||
Example:
|
||||
```
|
||||
[
|
||||
"apple",
|
||||
"banana",
|
||||
"cherry"
|
||||
]
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
|
||||
|
||||
# Quiz Game Module
|
||||
|
||||
This module implements a multiplayer quiz game for the Meshtastic mesh-bot.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
The quizmaster starts the quiz session (usually with `/quiz start` or similar command).
|
||||
- **Join the Game:**
|
||||
Players join by sending `/quiz join` or by answering a question while a quiz is active.
|
||||
- **Answer Questions:**
|
||||
- Use `Q: <answer>` to answer the current question.
|
||||
- For multiple choice, answer with `A`, `B`, `C`, etc.
|
||||
- For free-text, type the answer after `Q: `.
|
||||
- Use `Q: ?` to request the next question.
|
||||
- **Leave the Game:**
|
||||
Players can leave at any time with `/quiz leave`.
|
||||
- **Stop the Game:**
|
||||
The quizmaster stops the quiz session (e.g., `/quiz stop`). Final scores and the top 3 players are announced.
|
||||
|
||||
## Rules & Features
|
||||
|
||||
- Only the quizmaster can start or stop the quiz.
|
||||
- Players can join or leave at any time while the quiz is active.
|
||||
- Questions are loaded from quiz_questions.json and can be multiple choice or free-text.
|
||||
- Players earn 1 point for each correct answer.
|
||||
- The first player to answer each question correctly is noted.
|
||||
- The top 3 players are displayed at the end of the quiz.
|
||||
- The quizmaster can broadcast messages to all players.
|
||||
|
||||
## Example Commands
|
||||
|
||||
- Start quiz:
|
||||
`/quiz start`
|
||||
- Join quiz:
|
||||
`/quiz join`
|
||||
- Answer a question:
|
||||
`Q: B`
|
||||
`Q: Paris`
|
||||
- Next question:
|
||||
`Q: ?`
|
||||
- Leave quiz:
|
||||
`/quiz leave`
|
||||
- Stop quiz:
|
||||
`/quiz stop`
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one quiz can be active at a time.
|
||||
- Players can only answer each question once.
|
||||
- The quizmaster is defined by the `bbs_admin_list` variable.
|
||||
- Questions must be formatted correctly in the JSON file for the game to function.
|
||||
|
||||
---
|
||||
|
||||
**Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025**
|
||||
|
||||
Certainly! Here’s documentation for the **Survey Game Module** in the same format as your other game modules:
|
||||
|
||||
---
|
||||
|
||||
# Survey Module "game"
|
||||
This module implements a survey system for the Meshtastic mesh-bot.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Survey:**
|
||||
Users start a survey by specifying the survey name (e.g., `/survey start example`).
|
||||
The survey will prompt the user with the first question.
|
||||
|
||||
- **Answer Questions:**
|
||||
- For multiple choice: reply with a letter (A, B, C, ...).
|
||||
- For integer: reply with a number.
|
||||
- For text: reply with your answer as text.
|
||||
After each answer, the next question is shown automatically.
|
||||
|
||||
- **End the Survey:**
|
||||
The survey ends automatically after the last question, or the user can send `end` to finish early.
|
||||
Responses are saved to a CSV file.
|
||||
|
||||
## Rules & Features
|
||||
|
||||
- Surveys are defined in JSON files in surveys (e.g., `example_survey.json`).
|
||||
- Each survey can have multiple choice, integer, or text questions.
|
||||
- User responses are saved to a CSV file named `<survey_name>_responses.csv` in the same directory.
|
||||
- Users can only answer each question once per survey session.
|
||||
- Survey results can be summarized and reported by the bot.
|
||||
|
||||
## Example Commands
|
||||
|
||||
- Start a survey:
|
||||
`/survey start example`
|
||||
- Answer a multiple choice question:
|
||||
`A`
|
||||
- Answer an integer question:
|
||||
`42`
|
||||
- Answer a text question:
|
||||
`My favorite color is blue.`
|
||||
- End the survey early:
|
||||
`end`
|
||||
- Get survey results (admin):
|
||||
`/survey results example`
|
||||
|
||||
## Notes
|
||||
|
||||
- Only surveys listed in the surveys directory with the `_survey.json` suffix are available.
|
||||
- Each user’s responses are tracked separately.
|
||||
- Results are summarized and can be displayed by the bot.
|
||||
|
||||
---
|
||||
|
||||
**Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025**
|
||||
@@ -2,12 +2,12 @@
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
from random import choices, shuffle
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import jackTracker
|
||||
import time
|
||||
import pickle
|
||||
|
||||
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
|
||||
from modules.settings import jackTracker
|
||||
|
||||
SUITS = ("♥️", "♦️", "♠️", "♣️")
|
||||
RANKS = (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
|
||||
# Global variables
|
||||
total_days = 7 # number of days or rotations the player has to play
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
|
||||
# Clubs setup
|
||||
driver_distances = list(range(230, 280, 5))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
|
||||
class HamTest:
|
||||
def __init__(self):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
|
||||
from modules.log import logger, getPrettyTime
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
|
||||
class Hangman:
|
||||
@@ -118,6 +121,25 @@ class Hangman:
|
||||
|
||||
def __init__(self):
|
||||
self.game = {}
|
||||
self.DEFAULT_WORDS = self.WORDS
|
||||
|
||||
|
||||
# Try to load hangman.json if it exists
|
||||
hangman_json_path = os.path.join('data', 'hangman.json')
|
||||
if os.path.exists(hangman_json_path):
|
||||
try:
|
||||
with open(hangman_json_path, 'r') as f:
|
||||
words = json.load(f)
|
||||
# Ensure it's a list of strings
|
||||
if isinstance(words, list) and all(isinstance(w, str) for w in words):
|
||||
self.WORDS = words
|
||||
else:
|
||||
self.WORDS = self.DEFAULT_WORDS
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
logger.warning("Failed to load hangman.json, using default words. example JSON: [\"apple\",\"banana\",\"cherry\"]")
|
||||
self.WORDS = self.DEFAULT_WORDS
|
||||
else:
|
||||
self.WORDS = self.DEFAULT_WORDS
|
||||
|
||||
def new_game(self, id):
|
||||
games = won = 0
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# As a Ham, is this obsecuring the meaning of the joke? Or is it enhancing it?
|
||||
from dadjokes import Dadjoke # pip install dadjokes
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
from modules.settings import dad_jokes_emojiJokes, dad_jokes_enabled
|
||||
|
||||
lameJokes = [
|
||||
"Why don't scientists trust atoms? Because they make up everything!",
|
||||
@@ -177,10 +178,12 @@ def sendWithEmoji(message):
|
||||
i += 1
|
||||
return ' '.join(words)
|
||||
|
||||
def tell_joke(nodeID=0, vox=False):
|
||||
def tell_joke(nodeID=0, vox=False, test=False):
|
||||
dadjoke = Dadjoke()
|
||||
if test:
|
||||
return sendWithEmoji(dadjoke.joke)
|
||||
try:
|
||||
if dad_jokes_emojiJokes or vox:
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
|
||||
@@ -6,8 +6,8 @@ from random import randrange, uniform # random numbers
|
||||
from types import SimpleNamespace # namespaces support
|
||||
import pickle # pickle file support
|
||||
import time # time functions
|
||||
from modules.log import * # mesh-bot logging
|
||||
|
||||
from modules.log import logger # mesh-bot logging
|
||||
from modules.system import lemonadeTracker # player tracking
|
||||
import locale # culture specific locale
|
||||
import math # math functions
|
||||
import re # regular expressions
|
||||
@@ -23,7 +23,6 @@ lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0
|
||||
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
|
||||
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
|
||||
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
|
||||
from modules.settings import lemonadeTracker
|
||||
|
||||
def get_sales_amount(potential, unit, price):
|
||||
"""Gets the sales amount.
|
||||
|
||||
@@ -12,7 +12,7 @@ Game Rules:
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
from datetime import datetime, timedelta
|
||||
from geopy.distance import geodesic
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from modules.settings import mindTracker
|
||||
from modules.log import logger
|
||||
from modules.system import mindTracker
|
||||
|
||||
def chooseDifficultyMMind(message):
|
||||
usrInput = message.lower()
|
||||
msg = ''
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import bbs_admin_list
|
||||
|
||||
QUIZ_JSON = os.path.join(os.path.dirname(__file__), '../', '../', 'data', 'quiz_questions.json')
|
||||
QUIZMASTER_ID = bbs_admin_list
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Tic-Tac-Toe game for Meshtastic mesh-bot
|
||||
# Board positions chosen by numbers 1-9
|
||||
# 2025
|
||||
from modules.log import *
|
||||
import random
|
||||
import time
|
||||
import modules.settings as my_settings
|
||||
|
||||
# to (max), molly and jake, I miss you both so much.
|
||||
|
||||
if disable_emojis_in_games:
|
||||
if my_settings.disable_emojis_in_games:
|
||||
X = "X"
|
||||
O = "O"
|
||||
else:
|
||||
@@ -65,7 +65,7 @@ class TicTacToe:
|
||||
row = ""
|
||||
for j in range(3):
|
||||
pos = i * 3 + j
|
||||
if disable_emojis_in_games:
|
||||
if my_settings.disable_emojis_in_games:
|
||||
cell = b[pos] if b[pos] != " " else str(pos + 1)
|
||||
else:
|
||||
cell = b[pos] if b[pos] != " " else f" {str(pos + 1)} "
|
||||
@@ -74,7 +74,6 @@ class TicTacToe:
|
||||
row += " | "
|
||||
board_str += row
|
||||
if i < 2:
|
||||
#board_str += "\n-+-+-\n"
|
||||
board_str += "\n"
|
||||
|
||||
return board_str + "\n"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
|
||||
vpStartingCash = 20
|
||||
from modules.settings import vpTracker
|
||||
# Define the Card class
|
||||
class CardVP:
|
||||
global vpTracker
|
||||
|
||||
card_values = { # value of the ace is high until it needs to be low
|
||||
2: 2,
|
||||
@@ -296,154 +296,159 @@ def loadHSVp():
|
||||
return 0
|
||||
|
||||
def playVideoPoker(nodeID, message):
|
||||
global vpTracker, vpStartingCash
|
||||
msg = ""
|
||||
try:
|
||||
# Initialize the player
|
||||
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID) == "":
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: VideoPoker: New Player {nodeID}")
|
||||
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
|
||||
return f"You have {vpStartingCash} coins, \nWhats your bet?"
|
||||
|
||||
# Gather the player's bet
|
||||
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
|
||||
# Initialize shuffled Deck and Player
|
||||
player = PlayerVP()
|
||||
deck = DeckVP()
|
||||
deck.shuffle()
|
||||
drawCount = 1
|
||||
bet = 0
|
||||
msg = ''
|
||||
|
||||
# Initialize the player
|
||||
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID=nodeID) == "":
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: VideoPoker: New Player {nodeID}")
|
||||
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
|
||||
return f"You have {vpStartingCash} coins, \nWhats your bet?"
|
||||
|
||||
# Gather the player's bet
|
||||
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
|
||||
# Initialize shuffled Deck and Player
|
||||
player = PlayerVP()
|
||||
deck = DeckVP()
|
||||
deck.shuffle()
|
||||
drawCount = 1
|
||||
bet = 0
|
||||
msg = ''
|
||||
|
||||
# load the player bankroll from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player.bankroll = vpTracker[i]['cash']
|
||||
vpTracker[i]['time'] = time.time()
|
||||
|
||||
# Detect if message is a bet
|
||||
try:
|
||||
bet = int(message)
|
||||
except ValueError:
|
||||
msg += f"Please enter a valid bet, 1 to 5 coins. you have {player.bankroll} coins."
|
||||
|
||||
# Check if bet is valid
|
||||
if bet > player.bankroll:
|
||||
msg += f"You can only bet the money you have. {player.bankroll} coins, No strip poker here..."
|
||||
elif bet < 1:
|
||||
msg += "You must bet at least 1 coin.🪙"
|
||||
elif bet > 5:
|
||||
msg += "The 🎰 coin slot only fits 5 coins max."
|
||||
|
||||
# if msg contains an error, return it
|
||||
if msg is not None and msg != '':
|
||||
return msg
|
||||
else:
|
||||
# Take the bet
|
||||
player.bet(str(message))
|
||||
# Bet placed, start the game
|
||||
setLastCmdVp(nodeID, "playing")
|
||||
|
||||
# save player and deck to tracker
|
||||
# load the player bankroll from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
player.bankroll = vpTracker[i]['cash']
|
||||
vpTracker[i]['time'] = time.time()
|
||||
|
||||
# Play the game
|
||||
if getLastCmdVp(nodeID) == "playing":
|
||||
msg = ''
|
||||
|
||||
player.draw_cards(deck)
|
||||
msg += player.show_hand()
|
||||
# give hint to player
|
||||
msg += player.score_hand(resetHand=False)
|
||||
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
# Detect if message is a bet
|
||||
try:
|
||||
bet = int(message)
|
||||
except ValueError:
|
||||
msg += f"Please enter a valid bet, 1 to 5 coins. you have {player.bankroll} coins."
|
||||
|
||||
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll (H)and"
|
||||
setLastCmdVp(nodeID, "redraw")
|
||||
return msg
|
||||
|
||||
if getLastCmdVp(nodeID) == "redraw":
|
||||
msg = ''
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
drawCount = vpTracker[i]['drawCount']
|
||||
# Check if bet is valid
|
||||
if bet > player.bankroll:
|
||||
msg += f"You can only bet the money you have. {player.bankroll} coins, No strip poker here..."
|
||||
elif bet < 1:
|
||||
msg += "You must bet at least 1 coin.🪙"
|
||||
elif bet > 5:
|
||||
msg += "The 🎰 coin slot only fits 5 coins max."
|
||||
|
||||
# if msg contains an error, return it
|
||||
if msg is not None and msg != '':
|
||||
return msg
|
||||
else:
|
||||
# Take the bet
|
||||
player.bet(str(message))
|
||||
# Bet placed, start the game
|
||||
setLastCmdVp(nodeID, "playing")
|
||||
|
||||
# if player wants to redraw cards, and not done already
|
||||
if message.lower().startswith("n"):
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
if message.lower().startswith("h"):
|
||||
msg = player.show_hand()
|
||||
return msg
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
drawCount += 1
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
|
||||
# Play the game
|
||||
if getLastCmdVp(nodeID) == "playing":
|
||||
msg = ''
|
||||
|
||||
player.draw_cards(deck)
|
||||
msg += player.show_hand()
|
||||
# give hint to player
|
||||
msg += player.score_hand(resetHand=False)
|
||||
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
|
||||
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll (H)and"
|
||||
setLastCmdVp(nodeID, "redraw")
|
||||
return msg
|
||||
|
||||
if getLastCmdVp(nodeID) == "redraw":
|
||||
msg = ''
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
drawCount = vpTracker[i]['drawCount']
|
||||
|
||||
# if player wants to redraw cards, and not done already
|
||||
if message.lower().startswith("n"):
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
if message.lower().startswith("h"):
|
||||
msg = player.show_hand()
|
||||
return msg
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
drawCount += 1
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
else:
|
||||
# show redrawn hand
|
||||
return msg
|
||||
else:
|
||||
# show redrawn hand
|
||||
return msg
|
||||
else:
|
||||
# redraw already done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
|
||||
if getLastCmdVp(nodeID) == "endGame":
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
# redraw already done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
|
||||
if getLastCmdVp(nodeID) == "endGame":
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
|
||||
msg += player.score_hand()
|
||||
msg += player.score_hand()
|
||||
|
||||
if player.bankroll < 1:
|
||||
player.bankroll = vpStartingCash
|
||||
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
elif player.bankroll > vpTracker[i]['highScore']:
|
||||
vpTracker[i]['highScore'] = player.bankroll
|
||||
msg += " 🎉HighScore!"
|
||||
# save high score
|
||||
saveHSVp(nodeID, vpTracker[i]['highScore'])
|
||||
if player.bankroll < 1:
|
||||
player.bankroll = vpStartingCash
|
||||
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
elif player.bankroll > vpTracker[i]['highScore']:
|
||||
vpTracker[i]['highScore'] = player.bankroll
|
||||
msg += " 🎉HighScore!"
|
||||
# save high score
|
||||
saveHSVp(nodeID, vpTracker[i]['highScore'])
|
||||
|
||||
msg += f"\nPlace your Bet, or (L)eave Table."
|
||||
msg += f"\nPlace your Bet, or (L)eave Table."
|
||||
|
||||
setLastCmdVp(nodeID, "gameOver")
|
||||
# reset player and deck in tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = None
|
||||
vpTracker[i]['deck'] = None
|
||||
vpTracker[i]['drawCount'] = 0
|
||||
# save bankroll
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
setLastCmdVp(nodeID, "gameOver")
|
||||
# reset player and deck in tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = None
|
||||
vpTracker[i]['deck'] = None
|
||||
vpTracker[i]['drawCount'] = 0
|
||||
# save bankroll
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
|
||||
return msg
|
||||
return msg
|
||||
# At the end of the try block, if nothing returned yet:
|
||||
return msg if msg else 'No action taken.'
|
||||
except Exception as e:
|
||||
logger.warning(f"System: VideoPoker: Error {e}")
|
||||
return 'No Game in progress'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# python word of the day game module for meshing-around bot
|
||||
# 2025 K7MHI Kelly Keeton
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
import random
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
from geopy.geocoders import Nominatim # pip install geopy
|
||||
import maidenhead as mh # pip install maidenhead
|
||||
#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 *
|
||||
#import xml.dom.minidom
|
||||
from modules.log import logger
|
||||
from modules.settings import urlTimeoutSeconds, NO_ALERTS, myRegionalKeysDE
|
||||
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
trap_list_location_de = ("dealert", "dewx", "deflood")
|
||||
trap_list_location_eu = ("ukalert")
|
||||
trap_list_location_de = ("dealert")
|
||||
|
||||
def get_govUK_alerts(lat, lon):
|
||||
try:
|
||||
@@ -22,7 +23,7 @@ def get_govUK_alerts(lat, lon):
|
||||
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
|
||||
return
|
||||
|
||||
if alert:
|
||||
return "🚨" + alert.get_text(strip=True)
|
||||
@@ -47,7 +48,7 @@ def get_nina_alerts():
|
||||
return NO_ALERTS
|
||||
|
||||
def get_wxUKgov():
|
||||
# get UK weather warnings
|
||||
# get UK weather warnings, these look icky
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
|
||||
try:
|
||||
@@ -71,7 +72,62 @@ def get_wxUKgov():
|
||||
|
||||
|
||||
def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
# get UK flood warnings, there is so much I need a locals help
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
|
||||
def get_crimeUKgov(lat, lon):
|
||||
"""
|
||||
Fetches recent street crime data from UK Police API for given lat/lon.
|
||||
Returns a summary string or NO_ALERTS. -- pay for use?
|
||||
"""
|
||||
date = datetime.datetime.now().strftime("%Y-%m")
|
||||
url = f'https://data.police.uk/api/crimes-street/all-crime?date={date}&lat={lat}&lng={lon}'
|
||||
try:
|
||||
response = requests.get(url, timeout=urlTimeoutSeconds)
|
||||
if not response.ok or not response.text.strip():
|
||||
return NO_ALERTS
|
||||
crimes = response.json()
|
||||
if not crimes:
|
||||
return NO_ALERTS
|
||||
# Summarize the first few crimes
|
||||
summaries = []
|
||||
for crime in crimes[:3]:
|
||||
category = crime.get("category", "Unknown")
|
||||
outcome = crime.get("outcome_status", {}).get("category", "No outcome")
|
||||
location = crime.get("location", {}).get("street", {}).get("name", "Unknown location")
|
||||
summaries.append(f"{category.title()} at {location} ({outcome})")
|
||||
return "\n".join(summaries)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching UK crime data: {e}")
|
||||
return NO_ALERTS
|
||||
|
||||
def get_crime_stopsUKgov(lat, lon):
|
||||
"""
|
||||
Fetches recent stop-and-search data from UK Police API for given lat/lon.
|
||||
Returns a summary string or NO_ALERTS. -- pay for use?
|
||||
"""
|
||||
date = datetime.datetime.now().strftime("%Y-%m")
|
||||
url = f'https://data.police.uk/api/stops-street?date={date}&lat={lat}&lng={lon}'
|
||||
try:
|
||||
response = requests.get(url, timeout=urlTimeoutSeconds)
|
||||
if not response.ok or not response.text.strip():
|
||||
return NO_ALERTS
|
||||
stops = response.json()
|
||||
if not stops:
|
||||
return NO_ALERTS
|
||||
# Summarize the first few stops
|
||||
summaries = []
|
||||
for stop in stops[:3]: # Limit to first 3 stops for brevity
|
||||
summary = (
|
||||
f"Date: {stop.get('datetime', 'N/A')}, "
|
||||
f"Outcome: {stop.get('outcome', 'N/A')}, "
|
||||
f"Ethnicity: {stop.get('self_defined_ethnicity', 'N/A')}, "
|
||||
f"Gender: {stop.get('gender', 'N/A')}, "
|
||||
f"Location: {stop.get('location', {}).get('street', {}).get('name', 'N/A')}"
|
||||
)
|
||||
summaries.append(summary)
|
||||
return "\n".join(summaries)
|
||||
except Exception as e:
|
||||
return NO_ALERTS
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# https://pythonhosted.org/RPIO/
|
||||
import RPIO
|
||||
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
|
||||
|
||||
# set up input channel without pull-up
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# LLM Module for meshing-around
|
||||
# This module is used to interact with LLM API to generate responses to user input
|
||||
# K7MHI Kelly Keeton 2024
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import llmModel, ollamaHostName, rawLLMQuery
|
||||
|
||||
# Ollama Client
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
@@ -178,16 +179,21 @@ def get_google_context(input, num_results):
|
||||
|
||||
def send_ollama_query(llmQuery):
|
||||
# Send the query to the Ollama API and return the response
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
# deepseek has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
return result
|
||||
try:
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery), timeout=5)
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
# deepseek has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
logger.warning(f"System: LLM Query: Ollama API returned status code {result.status_code}")
|
||||
return f"⛔️ Request Error"
|
||||
return result
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"System: LLM Query: Ollama API request failed: {e}")
|
||||
return f"⛔️ Request Error"
|
||||
|
||||
def send_ollama_tooling_query(prompt, functions, model=None, max_tokens=450):
|
||||
"""
|
||||
|
||||
@@ -6,14 +6,15 @@ 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
|
||||
import xml.dom.minidom # used for parsing XML
|
||||
import xml.parsers.expat # used for parsing XML
|
||||
from datetime import datetime
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
import modules.settings as my_settings
|
||||
import math
|
||||
import csv
|
||||
import os
|
||||
|
||||
|
||||
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar", "map",)
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
@@ -23,7 +24,7 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
|
||||
if int(float(lat)) == 0 and int(float(lon)) == 0:
|
||||
logger.error("Location: No GPS data, try sending location")
|
||||
return NO_DATA_NOGPS
|
||||
return my_settings.NO_DATA_NOGPS
|
||||
|
||||
# initialize Nominatim API
|
||||
geolocator = Nominatim(user_agent="mesh-bot")
|
||||
@@ -43,7 +44,7 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = location.raw['address'].get('postcode', '')
|
||||
return whereIam
|
||||
|
||||
if float(lat) == latitudeValue and float(lon) == longitudeValue:
|
||||
if float(lat) == my_settings.latitudeValue and float(lon) == my_settings.longitudeValue:
|
||||
# redacted address when no GPS and using default location
|
||||
location = geolocator.reverse(str(lat) + ", " + str(lon))
|
||||
address = location.raw['address']
|
||||
@@ -72,7 +73,7 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
return whereIam
|
||||
except Exception as e:
|
||||
logger.debug("Location:Error fetching location data with whereami, likely network error")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
def getRepeaterBook(lat=0, lon=0):
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
@@ -90,7 +91,7 @@ def getRepeaterBook(lat=0, lon=0):
|
||||
try:
|
||||
msg = ''
|
||||
user_agent = {'User-agent': 'Mozilla/5.0'}
|
||||
response = requests.get(repeater_url, headers=user_agent, timeout=urlTimeoutSeconds)
|
||||
response = requests.get(repeater_url, headers=user_agent, timeout=my_settings.urlTimeoutSeconds)
|
||||
if response.status_code!=200:
|
||||
logger.error(f"Location:Error fetching repeater data from {repeater_url} with status code {response.status_code}")
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
@@ -129,13 +130,13 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
#grid = mh.to_maiden(float(lat), float(lon))
|
||||
repeaters = []
|
||||
zipCode = where_am_i(lat, lon, zip=True)
|
||||
if zipCode == NO_DATA_NOGPS or zipCode == ERROR_FETCHING_DATA:
|
||||
if zipCode == my_settings.NO_DATA_NOGPS or zipCode == my_settings.ERROR_FETCHING_DATA:
|
||||
return zipCode
|
||||
|
||||
if zipCode.isnumeric():
|
||||
try:
|
||||
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
|
||||
response = requests.get(artsci_url, timeout=urlTimeoutSeconds)
|
||||
response = requests.get(artsci_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if response.status_code!=200:
|
||||
logger.error(f"Location:Error fetching data from {artsci_url} with status code {response.status_code}")
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
@@ -177,16 +178,16 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
|
||||
try:
|
||||
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
|
||||
station_data = requests.get(station_lookup_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if station_data.ok:
|
||||
station_json = station_data.json()
|
||||
else:
|
||||
logger.error("Location:Error fetching tide station table from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
if station_json['stationList'] == [] or station_json['stationList'] is None:
|
||||
logger.error("Location:No tide station found")
|
||||
@@ -196,26 +197,26 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError):
|
||||
logger.error("Location:Error fetching tide station table from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
station_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&time_zone=lst_ldt&datum=MLLW&product=predictions&interval=hilo&format=json&station=" + station_id
|
||||
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
station_url += "&units=metric"
|
||||
else:
|
||||
station_url += "&units=english"
|
||||
|
||||
try:
|
||||
tide_data = requests.get(station_url, timeout=urlTimeoutSeconds)
|
||||
tide_data = requests.get(station_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if tide_data.ok:
|
||||
tide_json = tide_data.json()
|
||||
else:
|
||||
logger.error("Location:Error fetching tide data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError):
|
||||
logger.error("Location:Error fetching tide data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
tide_data = tide_json['predictions']
|
||||
|
||||
@@ -225,7 +226,7 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
tide_table = "Tide Data for " + tide_date + "\n"
|
||||
for tide in tide_data:
|
||||
tide_time = tide['t'].split(" ")[1]
|
||||
if not zuluTime:
|
||||
if not my_settings.zuluTime:
|
||||
# convert to 12 hour clock
|
||||
if int(tide_time.split(":")[0]) > 12:
|
||||
tide_time = str(int(tide_time.split(":")[0]) - 12) + ":" + tide_time.split(":")[1] + " PM"
|
||||
@@ -237,16 +238,17 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
tide_table = tide_table[:-1]
|
||||
return tide_table
|
||||
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0, report_days=None):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
weather = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
if report_days is None:
|
||||
report_days = my_settings.forecastDuration
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
unit = 1
|
||||
logger.debug("Location: new API metric units not implemented yet")
|
||||
|
||||
@@ -254,29 +256,29 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
|
||||
# extract the "forecast": property from the JSON response
|
||||
try:
|
||||
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
|
||||
weather_data = requests.get(weather_api, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not weather_data.ok:
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception:
|
||||
logger.warning(f"Location:Error fetching weather data error: {Exception}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
# get the forecast URL from the JSON response
|
||||
weather_json = weather_data.json()
|
||||
forecast_url = weather_json['properties']['forecast']
|
||||
try:
|
||||
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
|
||||
forecast_data = requests.get(forecast_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not forecast_data.ok:
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception:
|
||||
logger.warning(f"Location:Error fetching weather data error: {Exception}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
|
||||
forecast_json = forecast_data.json()
|
||||
forecast = forecast_json['properties']['periods']
|
||||
for day in forecast[:forecastDuration]:
|
||||
for day in forecast[:report_days]:
|
||||
# abreviate the forecast
|
||||
|
||||
weather += abbreviate_noaa(day['name']) + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
|
||||
@@ -286,7 +288,7 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get any alerts and return the count
|
||||
alerts = getWeatherAlertsNOAA(lat, lon)
|
||||
|
||||
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
|
||||
if alerts == my_settings.ERROR_FETCHING_DATA or alerts == my_settings.NO_DATA_NOGPS or alerts == my_settings.NO_ALERTS:
|
||||
alert = ""
|
||||
alert_num = 0
|
||||
else:
|
||||
@@ -395,36 +397,36 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
return NO_DATA_NOGPS
|
||||
return my_settings.NO_DATA_NOGPS
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
#logger.debug("Location:Fetching weather alerts from NOAA for " + str(lat) + ", " + str(lon))
|
||||
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
alert_data = requests.get(alert_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception:
|
||||
logger.warning(f"Location:Error fetching weather data error: {Exception}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
alerts = ""
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
for i in alertxml.getElementsByTagName("entry"):
|
||||
title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue
|
||||
area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue
|
||||
if enableExtraLocationWx:
|
||||
if my_settings.enableExtraLocationWx:
|
||||
alerts += f"{title}. {area_desc.replace(' ', '')}\n"
|
||||
else:
|
||||
alerts += f"{title}\n"
|
||||
|
||||
if alerts == "" or alerts == None:
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
@@ -437,23 +439,23 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# return the first ALERT_COUNT alerts
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
data = "\n".join(alerts.split("\n")[:my_settings.numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCacheNOAA = ""
|
||||
def alertBrodcastNOAA():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCacheNOAA
|
||||
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
|
||||
currentAlert = getWeatherAlertsNOAA(my_settings.latitudeValue, my_settings.longitudeValue)
|
||||
# check if any reason to discard the alerts
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
|
||||
if currentAlert == my_settings.ERROR_FETCHING_DATA or currentAlert == my_settings.NO_DATA_NOGPS:
|
||||
return False
|
||||
elif currentAlert == NO_ALERTS:
|
||||
elif currentAlert == my_settings.NO_ALERTS:
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
if ignoreEASenable:
|
||||
if my_settings.ignoreEASenable:
|
||||
# check if the alert is in the ignoreEAS list
|
||||
for word in ignoreEASwords:
|
||||
for word in my_settings.ignoreEASwords:
|
||||
if word.lower() in currentAlert[0].lower():
|
||||
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
|
||||
return False
|
||||
@@ -471,21 +473,21 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
#logger.debug("Location:Fetching weather alerts detailed from NOAA for " + str(lat) + ", " + str(lon))
|
||||
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
alert_data = requests.get(alert_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception:
|
||||
logger.warning(f"Location:Error fetching weather data error: {Exception}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
alerts = ""
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
@@ -505,10 +507,10 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# trim the alerts to the first ALERT_COUNT
|
||||
alerts = alerts.split("\n***\n")[:numWxAlerts]
|
||||
alerts = alerts.split("\n***\n")[:my_settings.numWxAlerts]
|
||||
|
||||
if alerts == "" or alerts == ['']:
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
@@ -530,13 +532,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
# get the alerts from FEMA
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
alert_data = requests.get(alert_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
@@ -558,13 +560,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
continue
|
||||
|
||||
# check if it matches your list
|
||||
if stateFips not in myStateFIPSList:
|
||||
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {myStateFIPSList}")
|
||||
if stateFips not in my_settings.myStateFIPSList:
|
||||
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {my_settings.myStateFIPSList}")
|
||||
continue # skip to next entry
|
||||
|
||||
try:
|
||||
# get the linked alert data from FEMA
|
||||
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
|
||||
linked_data = requests.get(link, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not linked_data.ok or not linked_data.text.strip():
|
||||
# if the linked data is not ok, skip this alert
|
||||
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
|
||||
@@ -616,14 +618,14 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
continue
|
||||
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
if (sameVal in my_settings.mySAMEList) or (geocode_value in my_settings.mySAMEList) or my_settings.mySAMEList == ['']:
|
||||
ignore_alert = False
|
||||
if ignoreFEMAenable:
|
||||
if my_settings.ignoreFEMAenable:
|
||||
ignore_alert = any(
|
||||
word.lower() in headline.lower()
|
||||
for word in ignoreFEMAwords)
|
||||
for word in my_settings.ignoreFEMAwords)
|
||||
if ignore_alert:
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {my_settings.ignoreFEMAwords} at {areaDesc}")
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
@@ -643,16 +645,16 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
for alertItem in alerts[:numWxAlerts]:
|
||||
for alertItem in alerts[:my_settings.numWxAlerts]:
|
||||
if shortAlerts:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}")
|
||||
else:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}\n{alertItem['description']}")
|
||||
# add a newline if not the last alert
|
||||
if alertItem != alerts[:numWxAlerts][-1]:
|
||||
if alertItem != alerts[:my_settings.numWxAlerts][-1]:
|
||||
alert += "\n"
|
||||
else:
|
||||
alert = NO_ALERTS
|
||||
alert = my_settings.NO_ALERTS
|
||||
|
||||
return alert
|
||||
|
||||
@@ -665,22 +667,22 @@ def get_flood_noaa(lat=0, lon=0, uid=None):
|
||||
headers = {'accept': 'application/json'}
|
||||
if not uid:
|
||||
logger.warning(f"Location:No flood gauge data found for UID {uid}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
try:
|
||||
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
|
||||
response = requests.get(api_url + str(uid), headers=headers, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not response.ok:
|
||||
logger.warning(f"Location:Error fetching flood gauge data from NOAA for {uid} (HTTP {response.status_code})")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
data = response.json()
|
||||
if not data or 'status' not in data:
|
||||
logger.warning(f"Location:No flood gauge data found for UID {uid}")
|
||||
return "No flood gauge data found"
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Location:Error fetching flood gauge data from: {api_url}{uid} ({e})")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.warning(f"Location:Unexpected error: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
# extract values from JSON safely
|
||||
try:
|
||||
@@ -696,35 +698,35 @@ def get_flood_noaa(lat=0, lon=0, uid=None):
|
||||
return flood_data
|
||||
except Exception as e:
|
||||
logger.debug(f"Location:Error extracting flood gauge data from NOAA for {uid}: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
def get_volcano_usgs(lat=0, lon=0):
|
||||
alerts = ''
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
# get the latest volcano alert from USGS from CAP feed
|
||||
usgs_volcano_url = "https://volcanoes.usgs.gov/hans-public/api/volcano/getCapElevated"
|
||||
try:
|
||||
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
|
||||
volcano_data = requests.get(usgs_volcano_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not volcano_data.ok:
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
volcano_json = volcano_data.json()
|
||||
# extract alerts from main feed
|
||||
if volcano_json and isinstance(volcano_json, list):
|
||||
for alert in volcano_json:
|
||||
# check ignore list
|
||||
if ignoreUSGSEnable:
|
||||
for word in ignoreUSGSwords:
|
||||
if my_settings.ignoreUSGSEnable:
|
||||
for word in my_settings.ignoreUSGSwords:
|
||||
if word.lower() in alert['volcano_name_appended'].lower():
|
||||
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
|
||||
continue
|
||||
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
|
||||
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
|
||||
if (alert['latitude'] >= my_settings.latitudeValue - 10 and alert['latitude'] <= my_settings.latitudeValue + 10) and (alert['longitude'] >= my_settings.longitudeValue - 10 and alert['longitude'] <= my_settings.longitudeValue + 10):
|
||||
volcano_name = alert['volcano_name_appended']
|
||||
alert_level = alert['alert_level']
|
||||
color_code = alert['color_code']
|
||||
@@ -737,9 +739,9 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
continue
|
||||
else:
|
||||
logger.debug("Location:Error fetching volcano data from USGS")
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
if alerts == "":
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
alerts = alerts[:-1]
|
||||
@@ -750,13 +752,13 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forecast from NWS coastal products
|
||||
try:
|
||||
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
marine_pz_data = requests.get(zone, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not marine_pz_data.ok:
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
logger.warning(f"Location:Error fetching NWS Marine data (HTTP {marine_pz_data.status_code})")
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Location:Error fetching NWS Marine data: {e}")
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
marine_pz_data = marine_pz_data.text
|
||||
todayDate = datetime.now().strftime("%Y%m%d")
|
||||
@@ -766,13 +768,13 @@ def get_nws_marine(zone, days=3):
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid or empty")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
marine_pzz_lines = marine_pz_data.split("\n")
|
||||
@@ -794,7 +796,7 @@ def get_nws_marine(zone, days=3):
|
||||
day_blocks.append(current_block.strip())
|
||||
|
||||
# Only keep up to pzDays blocks
|
||||
for block in day_blocks[:days]:
|
||||
for block in day_blocks[:my_settings.coastalForecastDays]:
|
||||
marine_pz_report += block + "\n"
|
||||
|
||||
# remove last newline
|
||||
@@ -808,13 +810,13 @@ def get_nws_marine(zone, days=3):
|
||||
# abbreviate the report
|
||||
marine_pz_report = abbreviate_noaa(marine_pz_report)
|
||||
if marine_pz_report == "":
|
||||
return NO_DATA_NOGPS
|
||||
return my_settings.NO_DATA_NOGPS
|
||||
return marine_pz_report
|
||||
|
||||
def checkUSGSEarthQuake(lat=0, lon=0):
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
lat = my_settings.latitudeValue
|
||||
lon = my_settings.longitudeValue
|
||||
radius = 100 # km
|
||||
magnitude = 1.5
|
||||
history = 7 # days
|
||||
@@ -824,20 +826,20 @@ def checkUSGSEarthQuake(lat=0, lon=0):
|
||||
quake_count = 0
|
||||
# fetch the earthquake data from USGS
|
||||
try:
|
||||
quake_data = requests.get(USGSquake_url, timeout=urlTimeoutSeconds)
|
||||
quake_data = requests.get(USGSquake_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not quake_data.ok:
|
||||
logger.warning("Location:Error fetching earthquake data from USGS")
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
if not quake_data.text.strip():
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
try:
|
||||
quake_xml = xml.dom.minidom.parseString(quake_data.text)
|
||||
except Exception as e:
|
||||
logger.warning(f"Location: USGS earthquake API returned invalid XML: {e}")
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching earthquake data from USGS")
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
|
||||
quake_xml = xml.dom.minidom.parseString(quake_data.text)
|
||||
quake_count = len(quake_xml.getElementsByTagName("event"))
|
||||
@@ -853,7 +855,7 @@ def checkUSGSEarthQuake(lat=0, lon=0):
|
||||
description_text = event.getElementsByTagName("description")[0].getElementsByTagName("text")[0].childNodes[0].nodeValue
|
||||
largest_mag = round(largest_mag, 1)
|
||||
if quake_count == 0:
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
else:
|
||||
return f"{quake_count} 🫨quakes in last {history} days within {radius} km. Largest: {largest_mag}M\n{description_text}"
|
||||
|
||||
@@ -867,7 +869,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
r = 6371 # Radius of earth in kilometers # haversine formula
|
||||
|
||||
if lat == 0 and lon == 0:
|
||||
return NO_DATA_NOGPS
|
||||
return my_settings.NO_DATA_NOGPS
|
||||
if nodeID == 0:
|
||||
return "No NodeID provided"
|
||||
|
||||
@@ -899,7 +901,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
c = 2 * math.asin(math.sqrt(a))
|
||||
|
||||
distance_km = c * r
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
msg += f"{distance_km:.2f} km"
|
||||
else:
|
||||
distance_miles = distance_km * 0.621371
|
||||
@@ -917,7 +919,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
time_diff = datetime.now() - last_point['time']
|
||||
if time_diff.total_seconds() > 60:
|
||||
hours = time_diff.total_seconds() / 3600
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
speed = distance_km / hours
|
||||
speed_str = f"{speed:.2f} km/h"
|
||||
else:
|
||||
@@ -941,7 +943,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
total_distance_km += c * r
|
||||
# add the distance from last point to current point
|
||||
total_distance_km += distance_km
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
msg += f", Total: {total_distance_km:.2f} km"
|
||||
else:
|
||||
total_distance_miles = total_distance_km * 0.621371
|
||||
@@ -974,7 +976,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
area = area * (6378137 ** 2) / 2.0
|
||||
area = abs(area) / 1e6 # convert to square kilometers
|
||||
|
||||
if use_metric:
|
||||
if my_settings.use_metric:
|
||||
msg += f", Area: {area:.2f} sq.km (approx)"
|
||||
else:
|
||||
area_miles = area * 0.386102
|
||||
@@ -1006,7 +1008,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
def get_openskynetwork(lat=0, lon=0):
|
||||
# get the latest aircraft data from OpenSky Network in the area
|
||||
if lat == 0 and lon == 0:
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
# setup a bounding box of 50km around the lat/lon
|
||||
box_size = 0.45 # approx 50km
|
||||
# return limits for aircraft search
|
||||
@@ -1019,16 +1021,16 @@ def get_openskynetwork(lat=0, lon=0):
|
||||
# fetch the aircraft data from OpenSky Network
|
||||
opensky_url = f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}&lamax={lamax}&lomax={lomax}"
|
||||
try:
|
||||
aircraft_data = requests.get(opensky_url, timeout=urlTimeoutSeconds)
|
||||
aircraft_data = requests.get(opensky_url, timeout=my_settings.urlTimeoutSeconds)
|
||||
if not aircraft_data.ok:
|
||||
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
|
||||
return ERROR_FETCHING_DATA
|
||||
return my_settings.ERROR_FETCHING_DATA
|
||||
aircraft_json = aircraft_data.json()
|
||||
if 'states' not in aircraft_json or not aircraft_json['states']:
|
||||
return NO_ALERTS
|
||||
return my_settings.NO_ALERTS
|
||||
aircraft_list = aircraft_json['states']
|
||||
aircraft_report = ""
|
||||
for aircraft in aircraft_list:
|
||||
@@ -1055,7 +1057,7 @@ def get_openskynetwork(lat=0, lon=0):
|
||||
if aircraft_report.endswith("\n"):
|
||||
aircraft_report = aircraft_report[:-1]
|
||||
aircraft_report = abbreviate_noaa(aircraft_report)
|
||||
return aircraft_report if aircraft_report else NO_ALERTS
|
||||
return aircraft_report if aircraft_report else my_settings.NO_ALERTS
|
||||
|
||||
def log_locationData_toMap(userID, location, message):
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from modules.settings import *
|
||||
import modules.settings as my_settings
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
LOGGING_LEVEL = "DEBUG"
|
||||
if not my_settings.LOGGING_LEVEL:
|
||||
my_settings.LOGGING_LEVEL = "DEBUG"
|
||||
|
||||
LOGGING_LEVEL = getattr(logging, LOGGING_LEVEL)
|
||||
LOGGING_LEVEL = getattr(logging, my_settings.LOGGING_LEVEL)
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
grey = '\x1b[38;21m'
|
||||
@@ -70,16 +70,16 @@ stdout_handler.setFormatter(CustomFormatter(logFormat))
|
||||
# Add handlers to the logger
|
||||
logger.addHandler(stdout_handler)
|
||||
|
||||
if syslog_to_file:
|
||||
if my_settings.syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=my_settings.log_backup_count, encoding='utf-8')
|
||||
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
if log_messages_to_file:
|
||||
if my_settings.log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=my_settings.log_backup_count, encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
# Module to respomnd to new nodes we havent seen before with a hello message
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import qrz_db
|
||||
|
||||
def initalize_qrz_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
# Check if the qrz table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS qrz
|
||||
(qrz_id INTEGER PRIMARY KEY, qrz_call TEXT, qrz_name TEXT, qrz_qth TEXT, qrz_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
try:
|
||||
# If the database file doesn't exist, it will be created by sqlite3.connect
|
||||
if not os.path.exists(qrz_db):
|
||||
logger.info(f"QRZ database file '{qrz_db}' not found. Creating new database.")
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
# Create the table if it doesn't exist
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS qrz
|
||||
(qrz_id INTEGER PRIMARY KEY, qrz_call TEXT, qrz_name TEXT, qrz_qth TEXT, qrz_notes TEXT)''')
|
||||
conn.commit()
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Error initializing QRZ database: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def never_seen_before(nodeID):
|
||||
# check if we have seen this node before and sent a hello message
|
||||
|
||||
@@ -5,8 +5,25 @@
|
||||
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
import asyncio
|
||||
from modules.settings import (
|
||||
radio_detection_enabled,
|
||||
rigControlServerAddress,
|
||||
signalDetectionThreshold,
|
||||
signalHoldTime,
|
||||
signalCooldown,
|
||||
signalCycleLimit,
|
||||
voxDetectionEnabled,
|
||||
useLocalVoxModel,
|
||||
localVoxModelPath,
|
||||
voxLanguage,
|
||||
voxInputDevice,
|
||||
voxTrapList,
|
||||
voxOnTrapList,
|
||||
voxEnableCmd,
|
||||
ERROR_FETCHING_DATA
|
||||
)
|
||||
|
||||
# verbose debug logging for trap words function
|
||||
debugVoxTmsg = False
|
||||
@@ -100,6 +117,9 @@ def get_freq_common_name(freq):
|
||||
|
||||
def get_hamlib(msg="f"):
|
||||
# get data from rigctld server
|
||||
if "socket" not in globals():
|
||||
logger.warning("RadioMon: 'socket' module not imported. Hamlib disabled.")
|
||||
return ERROR_FETCHING_DATA
|
||||
try:
|
||||
rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
rigControlSocket.settimeout(2)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# rss feed module for meshing-around 2025
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import rssFeedURL, rssFeedNames, rssMaxItems, rssTruncate, urlTimeoutSeconds, ERROR_FETCHING_DATA
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
import html
|
||||
from html.parser import HTMLParser
|
||||
import bs4 as bs
|
||||
|
||||
# Common User-Agent for all RSS requests
|
||||
COMMON_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
|
||||
|
||||
class MLStripper(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -52,42 +56,68 @@ def get_rss_feed(msg):
|
||||
if "?" in msg_lower:
|
||||
return f"Fetches the latest {RSS_RETURN_COUNT} entries RSS feeds. Available feeds are: {', '.join(RSS_FEED_NAMES)}. To fetch a specific feed, include its name in your request."
|
||||
|
||||
# Fetch and parse the RSS feed
|
||||
try:
|
||||
logger.debug(f"Fetching RSS feed from {feed_url} from message '{msg}'")
|
||||
agent = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
|
||||
agent = {'User-Agent': COMMON_USER_AGENT}
|
||||
request = urllib.request.Request(feed_url, headers=agent)
|
||||
with urllib.request.urlopen(request, timeout=urlTimeoutSeconds) as response:
|
||||
xml_data = response.read()
|
||||
root = ET.fromstring(xml_data)
|
||||
# Try both namespaced and non-namespaced item tags
|
||||
items = root.findall('.//item')
|
||||
ns = None
|
||||
if not items:
|
||||
# Try to find the namespace dynamically
|
||||
for elem in root.iter():
|
||||
if elem.tag.endswith('item'):
|
||||
ns_uri = elem.tag.split('}')[0].strip('{')
|
||||
items = root.findall(f'.//{{{ns_uri}}}item')
|
||||
ns = ns_uri
|
||||
break
|
||||
|
||||
# Find all <item> (RSS) and <entry> (Atom) elements, regardless of namespace
|
||||
items = []
|
||||
for elem in root.iter():
|
||||
if elem.tag.endswith('item') or elem.tag.endswith('entry'):
|
||||
items.append(elem)
|
||||
items = items[:RSS_RETURN_COUNT]
|
||||
|
||||
if not items:
|
||||
return "No RSS feed entries found."
|
||||
logger.debug(f"No RSS or Atom feed entries found in feed xml_data: {xml_data[:500]}...")
|
||||
return "No RSS or Atom feed entries found."
|
||||
|
||||
formatted_entries = []
|
||||
for item in items:
|
||||
if ns:
|
||||
title = item.findtext(f'{{{ns}}}title', default='No title')
|
||||
link = item.findtext(f'{{{ns}}}link', default=None)
|
||||
description = item.findtext(f'{{{ns}}}description', default='No description')
|
||||
pub_date = item.findtext(f'{{{ns}}}pubDate', default='No date')
|
||||
else:
|
||||
title = item.findtext('title', default='No title')
|
||||
link = item.findtext('link', default=None)
|
||||
description = item.findtext('description', default='No description')
|
||||
pub_date = item.findtext('pubDate', default='No date')
|
||||
# Helper to try multiple tag names
|
||||
def find_any(item, tags):
|
||||
for tag in tags:
|
||||
val = item.findtext(tag)
|
||||
if val:
|
||||
return val
|
||||
return None
|
||||
|
||||
title = find_any(item, [
|
||||
'title',
|
||||
'{http://purl.org/rss/1.0/}title',
|
||||
'{http://www.w3.org/2005/Atom}title'
|
||||
])
|
||||
|
||||
# Atom links are often attributes, not text
|
||||
link = find_any(item, [
|
||||
'link',
|
||||
'{http://purl.org/rss/1.0/}link',
|
||||
'{http://www.w3.org/2005/Atom}link'
|
||||
])
|
||||
if not link:
|
||||
link_elem = item.find('{http://www.w3.org/2005/Atom}link')
|
||||
if link_elem is not None and 'href' in link_elem.attrib:
|
||||
link = link_elem.attrib['href']
|
||||
|
||||
description = find_any(item, [
|
||||
'description',
|
||||
'{http://purl.org/rss/1.0/}description',
|
||||
'{http://purl.org/rss/1.0/modules/content/}encoded',
|
||||
'{http://www.w3.org/2005/Atom}summary',
|
||||
'{http://www.w3.org/2005/Atom}content'
|
||||
])
|
||||
pub_date = find_any(item, [
|
||||
'pubDate',
|
||||
'{http://purl.org/dc/elements/1.1/}date',
|
||||
'{http://www.w3.org/2005/Atom}updated'
|
||||
])
|
||||
|
||||
# Unescape HTML entities and strip tags
|
||||
description = html.unescape(description)
|
||||
description = html.unescape(description) if description else ""
|
||||
description = strip_tags(description)
|
||||
if len(description) > RSS_TRIM_LENGTH:
|
||||
description = description[:RSS_TRIM_LENGTH - 3] + "..."
|
||||
@@ -97,3 +127,4 @@ def get_rss_feed(msg):
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching RSS feed from {feed_url}: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
@@ -2,15 +2,75 @@
|
||||
# Scheduler setup for Mesh Bot
|
||||
import asyncio
|
||||
import schedule
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from modules.log import logger
|
||||
from modules.settings import MOTD
|
||||
from modules.system import send_message
|
||||
|
||||
async def setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
|
||||
|
||||
# methods available for custom scheduler messages
|
||||
async def run_scheduler_loop(interval=1):
|
||||
logger.debug(f"System: Scheduler loop started Tasks: {len(schedule.jobs)}, Details:{extract_schedule_fields(schedule.get_jobs())}")
|
||||
try:
|
||||
last_logged_minute = -1
|
||||
while True:
|
||||
try:
|
||||
# Log scheduled jobs every 20 minutes
|
||||
now = datetime.now()
|
||||
if now.minute % 20 == 0 and now.minute != last_logged_minute:
|
||||
logger.debug(f"System: Scheduled Tasks {len(schedule.jobs)}, Details:{extract_schedule_fields(schedule.get_jobs())}")
|
||||
last_logged_minute = now.minute
|
||||
schedule.run_pending()
|
||||
except Exception as e:
|
||||
logger.error(f"System: Scheduler loop exception: {e}")
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("System: Scheduler loop cancelled, shutting down.")
|
||||
|
||||
def safe_int(val, default=0, type=""):
|
||||
try:
|
||||
return int(val)
|
||||
except (ValueError, TypeError):
|
||||
logger.debug(f"System: Scheduler config {type} error '{val}' to int, using default {default}")
|
||||
return default
|
||||
|
||||
def extract_schedule_fields(jobs):
|
||||
"""
|
||||
Extracts 'Every ... (last run: [...], next run: ...)' from schedule.get_jobs() output without regex.
|
||||
"""
|
||||
jobs_str = str(jobs)
|
||||
results = []
|
||||
# Split by '), ' to separate jobs, then add ')' back except last
|
||||
parts = jobs_str.split('), ')
|
||||
for i, part in enumerate(parts):
|
||||
if not part.endswith(')'):
|
||||
part += ')'
|
||||
# Find the start of 'Every'
|
||||
start = part.find('Every')
|
||||
if start != -1:
|
||||
# Find the start of 'do <lambda>()'
|
||||
do_idx = part.find('do ')
|
||||
if do_idx != -1:
|
||||
summary = part[start:do_idx].strip()
|
||||
# Find the (last run: ... next run: ...) part
|
||||
paren_idx = part.find('(', do_idx)
|
||||
if paren_idx != -1:
|
||||
summary += ' ' + part[paren_idx:].strip()
|
||||
while '<function ' in summary:
|
||||
f_start = summary.find('<function ')
|
||||
f_end = summary.find('>', f_start)
|
||||
if f_end == -1:
|
||||
break
|
||||
func_str = summary[f_start+10:f_end]
|
||||
func_name = func_str.split(' ')[0]
|
||||
summary = summary[:f_start] + func_name + summary[f_end+1:]
|
||||
results.append(summary)
|
||||
return results
|
||||
|
||||
def setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval):
|
||||
try:
|
||||
# Methods imported from mesh_bot for scheduling tasks
|
||||
from mesh_bot import (
|
||||
tell_joke,
|
||||
welcome_message,
|
||||
@@ -23,74 +83,75 @@ async def setup_scheduler(
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.warning(f"Some mesh_bot schedule features are unavailable by option disable in config.ini: {e} comment out the use of these methods in your custom_scheduler.py")
|
||||
|
||||
|
||||
# Setup the scheduler based on configuration
|
||||
schedulerValue = schedulerValue.lower().strip()
|
||||
schedulerTime = schedulerTime.strip()
|
||||
schedulerInterval = schedulerInterval.strip()
|
||||
schedulerChannel = int(schedulerChannel)
|
||||
schedulerInterface = int(schedulerInterface)
|
||||
# Setup the scheduler based on configuration
|
||||
schedulerChannel = safe_int(schedulerChannel, 0, type="channel")
|
||||
schedulerInterface = safe_int(schedulerInterface, 1, type="interface")
|
||||
schedulerIntervalInt = safe_int(schedulerInterval, 5, type="interval")
|
||||
|
||||
try:
|
||||
if schedulerMotd:
|
||||
scheduler_message = MOTD
|
||||
else:
|
||||
scheduler_message = schedulerMessage
|
||||
scheduler_message = MOTD if schedulerMotd else schedulerMessage
|
||||
|
||||
def send_sched_msg():
|
||||
send_message(scheduler_message, schedulerChannel, 0, schedulerInterface)
|
||||
|
||||
# Basic Scheduler Options
|
||||
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
|
||||
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
|
||||
# Basic scheduler job to run the schedule see examples below for custom schedules
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
if any(option in schedulerValue for option in basicOptions):
|
||||
if schedulerValue == 'day':
|
||||
if schedulerTime:
|
||||
schedule.every().day.at(schedulerTime).do(send_sched_msg)
|
||||
else:
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'joke' in schedulerValue.lower():
|
||||
# Schedule to send a joke every specified interval
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'link' in schedulerValue.lower():
|
||||
# Schedule to send a link message every specified interval
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_satpass(schedulerInterface, 'link'), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the link scheduler to send link messages every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'weather' in schedulerValue.lower():
|
||||
# Schedule to send weather updates every specified interval
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'custom' in schedulerValue.lower():
|
||||
# Import and setup custom schedules from custom_scheduler.py
|
||||
schedule.every(schedulerIntervalInt).days.do(send_sched_msg)
|
||||
elif 'mon' in schedulerValue and schedulerTime:
|
||||
schedule.every().monday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'tue' in schedulerValue and schedulerTime:
|
||||
schedule.every().tuesday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'wed' in schedulerValue and schedulerTime:
|
||||
schedule.every().wednesday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'thu' in schedulerValue and schedulerTime:
|
||||
schedule.every().thursday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'fri' in schedulerValue and schedulerTime:
|
||||
schedule.every().friday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'sat' in schedulerValue and schedulerTime:
|
||||
schedule.every().saturday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'sun' in schedulerValue and schedulerTime:
|
||||
schedule.every().sunday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'hour' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).hours.do(send_sched_msg)
|
||||
elif 'min' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).minutes.do(send_sched_msg)
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerIntervalInt} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'joke' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).minutes.do(
|
||||
partial(send_message, tell_joke(), schedulerChannel, 0, schedulerInterface)
|
||||
)
|
||||
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerIntervalInt} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'link' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).hours.do(
|
||||
send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface)
|
||||
)
|
||||
logger.debug(f"System: Starting the link scheduler to send link messages every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'weather' in schedulerValue:
|
||||
schedule.every().day.at(schedulerTime).do(
|
||||
partial(send_message, handle_wxc(0, schedulerInterface, 'wx', days=1), schedulerChannel, 0, schedulerInterface)
|
||||
)
|
||||
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'custom' in schedulerValue:
|
||||
try:
|
||||
# This file is located in etc/custom_scheduler.py and copied to modules/custom_scheduler.py at install
|
||||
from modules.custom_scheduler import setup_custom_schedules # type: ignore # pylance
|
||||
from modules.custom_scheduler import setup_custom_schedules # type: ignore
|
||||
setup_custom_schedules(
|
||||
send_message, tell_joke, welcome_message, handle_wxc, MOTD,
|
||||
schedulerChannel, schedulerInterface)
|
||||
logger.debug("System: Custom scheduler file imported and custom schedules set up.")
|
||||
logger.debug(f"System: Starting the custom_scheduler.py ")
|
||||
schedule.every().monday.at("12:00").do(
|
||||
lambda: logger.info("System: Scheduled Broadcast Enabled Reminder")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Failed to import custom scheduler. {e}")
|
||||
logger.warning("Custom scheduler file not found or failed to import. cp etc/custom_scheduler.py modules/custom_scheduler.py")
|
||||
|
||||
# Start the Broadcast Scheduler
|
||||
await BroadcastScheduler()
|
||||
except Exception as e:
|
||||
logger.error(f"System: Scheduler Error {e}")
|
||||
|
||||
return True
|
||||
@@ -46,6 +46,13 @@ dwPlayerTracker = [] # DopeWars player tracker
|
||||
jackTracker = [] # Jack game tracker
|
||||
mindTracker = [] # Mastermind (mmind) game tracker
|
||||
|
||||
# Memory Management Constants
|
||||
MAX_MSG_HISTORY = 250
|
||||
MAX_CMD_HISTORY = 250
|
||||
MAX_SEEN_NODES = 1000
|
||||
CLEANUP_INTERVAL = 86400 # 24 hours in seconds
|
||||
GAMEDELAY = 3 * CLEANUP_INTERVAL # 3 days in seconds
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
config_file = "config.ini"
|
||||
@@ -378,6 +385,7 @@ try:
|
||||
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
dxspotter_enabled = config['radioMon'].getboolean('dxspotter_enabled', True) # default True
|
||||
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
|
||||
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
|
||||
sigWatchBroadcastInterface = config['radioMon'].getint('sigWatchBroadcastInterface', 1) # default interface 1
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
# https://avtech.com/articles/138/list-of-email-to-sms-addresses/
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import (
|
||||
SMTP_SERVER, SMTP_PORT, SMTP_AUTH, SMTP_USERNAME, SMTP_PASSWORD,
|
||||
FROM_EMAIL, EMAIL_SUBJECT, enableImap, IMAP_SERVER, IMAP_PORT,
|
||||
IMAP_USERNAME, IMAP_PASSWORD, IMAP_FOLDER, sysopEmails, bbs_ban_list
|
||||
)
|
||||
import pickle
|
||||
import time
|
||||
import smtplib
|
||||
|
||||
@@ -7,7 +7,10 @@ import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
import ephem # pip install pyephem
|
||||
from datetime import timezone
|
||||
from modules.log import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
from modules.settings import (latitudeValue, longitudeValue, zuluTime,
|
||||
n2yoAPIKey, urlTimeoutSeconds, use_metric,
|
||||
ERROR_FETCHING_DATA, NO_DATA_NOGPS, NO_ALERTS)
|
||||
import math
|
||||
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass", "howtall")
|
||||
|
||||
@@ -13,7 +13,8 @@ import os # For file operations
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import surveyRecordLocation, surveyRecordID
|
||||
|
||||
allowedSurveys = [] # List of allowed survey names
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import base64
|
||||
import contextlib # for suppressing output on watchdog
|
||||
import io # for suppressing output on watchdog
|
||||
# homebrew 'modules'
|
||||
from modules.log import *
|
||||
from modules.settings import *
|
||||
from modules.log import logger, getPrettyTime, CustomFormatter
|
||||
|
||||
# Global Variables
|
||||
trap_list = ("cmd","cmd?","bannode",) # base commands
|
||||
@@ -22,13 +23,6 @@ games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
interface_retry_count = 3
|
||||
|
||||
# Memory Management Constants
|
||||
MAX_MSG_HISTORY = 250
|
||||
MAX_CMD_HISTORY = 250
|
||||
MAX_SEEN_NODES = 1000
|
||||
CLEANUP_INTERVAL = 86400 # 24 hours in seconds
|
||||
GAMEDELAY = 3 * CLEANUP_INTERVAL # 3 days in seconds
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
# ping, pinging, ack, testing, test, pong
|
||||
@@ -147,11 +141,16 @@ if dad_jokes_enabled:
|
||||
trap_list = trap_list + ("joke",)
|
||||
help_message = help_message + ", joke"
|
||||
|
||||
if dxspotter_enabled:
|
||||
from modules.dxspot import handledxcluster
|
||||
trap_list = trap_list + ("dx",)
|
||||
help_message = help_message + ", dx"
|
||||
|
||||
# Wikipedia Search Configuration
|
||||
if wikipedia_enabled:
|
||||
from modules.wiki import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("wiki:",)
|
||||
help_message = help_message + ", wiki:"
|
||||
trap_list = trap_list + ("wiki",)
|
||||
help_message = help_message + ", wiki"
|
||||
|
||||
# RSS Feed Configuration
|
||||
if rssEnable:
|
||||
@@ -261,10 +260,6 @@ if games_enabled is True:
|
||||
else:
|
||||
gamesCmdList = ""
|
||||
|
||||
# Scheduled Broadcast Configuration
|
||||
if scheduler_enabled:
|
||||
import schedule # pip install schedule
|
||||
|
||||
# Sentry Configuration
|
||||
if sentry_enabled:
|
||||
from math import sqrt
|
||||
@@ -1398,16 +1393,6 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
|
||||
# if not a bot ID track it
|
||||
if nodeID != globals().get(f'myNodeNum{rxNode}') and nodeID != 0:
|
||||
# consider Meta for most messages leaderboard
|
||||
node_message_count = meshLeaderboard.get('nodeMessageCounts', {})
|
||||
node_message_count[nodeID] = node_message_count.get(nodeID, 0) + 1
|
||||
meshLeaderboard['nodeTMessageCounts'] = node_message_count
|
||||
|
||||
if node_message_count[nodeID] > meshLeaderboard['mostTMessages']['value']:
|
||||
meshLeaderboard['mostTMessages']['value'] = node_message_count[nodeID]
|
||||
meshLeaderboard['mostTMessages']['nodeID'] = nodeID
|
||||
meshLeaderboard['mostTMessages']['timestamp'] = time.time()
|
||||
|
||||
# consider Meta for highest and weakest DBm
|
||||
if packet.get('rxSnr') is not None:
|
||||
dbm = packet['rxSnr']
|
||||
@@ -1415,6 +1400,20 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
meshLeaderboard['highestDBm'] = {'nodeID': nodeID, 'value': dbm, 'timestamp': time.time()}
|
||||
if dbm < meshLeaderboard['weakestDBm']['value']:
|
||||
meshLeaderboard['weakestDBm'] = {'nodeID': nodeID, 'value': dbm, 'timestamp': time.time()}
|
||||
|
||||
# Meta for most Messages leaderboard
|
||||
if packet_type == 'TEXT_MESSAGE':
|
||||
message_count = meshLeaderboard.get('nodeMessageCounts', {})
|
||||
message_count[nodeID] = message_count.get(nodeID, 0) + 1
|
||||
meshLeaderboard['nodeMessageCounts'] = message_count
|
||||
if message_count[nodeID] > meshLeaderboard['mostMessages']['value']:
|
||||
meshLeaderboard['mostMessages'] = {'nodeID': nodeID, 'value': message_count[nodeID], 'timestamp': time.time()}
|
||||
else:
|
||||
tmessage_count = meshLeaderboard.get('nodeTMessageCounts', {})
|
||||
tmessage_count[nodeID] = tmessage_count.get(nodeID, 0) + 1
|
||||
meshLeaderboard['nodeTMessageCounts'] = tmessage_count
|
||||
if tmessage_count[nodeID] > meshLeaderboard['mostTMessages']['value']:
|
||||
meshLeaderboard['mostTMessages'] = {'nodeID': nodeID, 'value': tmessage_count[nodeID], 'timestamp': time.time()}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Metadata decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
@@ -1769,12 +1768,8 @@ def saveLeaderboard():
|
||||
def loadLeaderboard():
|
||||
global meshLeaderboard
|
||||
try:
|
||||
initializeMeshLeaderboard()
|
||||
defaults = meshLeaderboard.copy()
|
||||
with open('data/leaderboard.pkl', 'rb') as f:
|
||||
loaded = pickle.load(f)
|
||||
defaults.update(loaded) # loaded values overwrite defaults
|
||||
meshLeaderboard = defaults
|
||||
meshLeaderboard = pickle.load(f)
|
||||
if logMetaStats:
|
||||
logger.debug("System: Mesh Leaderboard loaded from leaderboard.pkl")
|
||||
except FileNotFoundError:
|
||||
@@ -1925,12 +1920,6 @@ def get_sysinfo(nodeID=0, deviceID=1):
|
||||
sysinfo += f"📊{stats}"
|
||||
return sysinfo
|
||||
|
||||
async def BroadcastScheduler():
|
||||
# handle schedule checks for the broadcast of messages
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def handleSignalWatcher():
|
||||
global lastHamLibAlert
|
||||
# monitor rigctld for signal strength and frequency
|
||||
@@ -2117,12 +2106,16 @@ async def process_vox_queue():
|
||||
async def watchdog():
|
||||
global localTelemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
logger.debug("System: Watchdog started")
|
||||
wd_last_logged_minute = -1
|
||||
while True:
|
||||
await asyncio.sleep(20)
|
||||
now = datetime.now()
|
||||
|
||||
# perform memory cleanup every 10 minutes
|
||||
if datetime.now().minute % 10 == 0:
|
||||
|
||||
if now.minute % 20 == 0 and now.minute != wd_last_logged_minute:
|
||||
# perform memory cleanup every 10 minutes
|
||||
cleanup_memory()
|
||||
wd_last_logged_minute = now.minute
|
||||
|
||||
# check all interfaces
|
||||
for i in range(1, 10):
|
||||
|
||||
412
modules/test_bot.py
Normal file
412
modules/test_bot.py
Normal file
@@ -0,0 +1,412 @@
|
||||
# test_bot.py
|
||||
# Unit tests for various modules in the meshing-around project
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to sys.path to allow module imports
|
||||
parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, parent_path)
|
||||
|
||||
import unittest
|
||||
import importlib
|
||||
import pkgutil
|
||||
import warnings
|
||||
from modules.log import logger
|
||||
from modules.settings import latitudeValue, longitudeValue
|
||||
# Suppress ResourceWarning warnings for asyncio unclosed event here
|
||||
warnings.filterwarnings("ignore", category=ResourceWarning)
|
||||
|
||||
|
||||
modules_path = os.path.join(parent_path, 'modules')
|
||||
|
||||
# Limits API calls during testing
|
||||
CHECKALL = False
|
||||
# Check for a file named .checkall in the parent directory
|
||||
checkall_path = os.path.join(parent_path, '.checkall')
|
||||
if os.path.isfile(checkall_path):
|
||||
CHECKALL = True
|
||||
|
||||
|
||||
# List of module names to exclude
|
||||
exclude = ['test_bot','udp', 'system', 'log', 'gpio', 'web',]
|
||||
available_modules = [
|
||||
m.name for m in pkgutil.iter_modules([modules_path])
|
||||
if m.name not in exclude]
|
||||
|
||||
try:
|
||||
print("\nImporting Core Modules:")
|
||||
from modules.log import logger, getPrettyTime
|
||||
print(" ✔ Imported 'log'")
|
||||
# Set location default
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
print(f" ✔ Location set to Latitude: {lat}, Longitude: {lon}")
|
||||
from modules.system import *
|
||||
print(" ✔ Imported 'system'")
|
||||
|
||||
print("\nImporting non-excluded modules:")
|
||||
for module_name in [m.name for m in pkgutil.iter_modules([modules_path])]:
|
||||
if module_name not in exclude:
|
||||
importlib.import_module(module_name)
|
||||
print(f" ✔ Imported '{module_name}'")
|
||||
except Exception as e:
|
||||
print(f"\nError importing modules: {e}")
|
||||
print("Run this program from the main program directory: python3 script/test_bot.py")
|
||||
exit(1)
|
||||
|
||||
class TestBot(unittest.TestCase):
|
||||
def test_example(self):
|
||||
# Example test case
|
||||
self.assertEqual(1 + 1, 2)
|
||||
|
||||
def test_load_bbsdb(self):
|
||||
from bbstools import load_bbsdb
|
||||
test_load = load_bbsdb()
|
||||
self.assertTrue(test_load)
|
||||
|
||||
def test_bbs_list_messages(self):
|
||||
from bbstools import bbs_list_messages
|
||||
messages = bbs_list_messages()
|
||||
print("list_messages() returned:", messages)
|
||||
self.assertIsInstance(messages, str)
|
||||
|
||||
def test_initialize_checklist_database(self):
|
||||
from checklist import initialize_checklist_database, process_checklist_command
|
||||
result = initialize_checklist_database()
|
||||
result1 = process_checklist_command(0, 'checklist', name="none", location="none")
|
||||
self.assertTrue(result)
|
||||
self.assertIsInstance(result1, str)
|
||||
|
||||
def test_init_news_sources(self):
|
||||
from filemon import initNewsSources
|
||||
result = initNewsSources()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_get_nina_alerts(self):
|
||||
from globalalert import get_nina_alerts
|
||||
alerts = get_nina_alerts()
|
||||
self.assertIsInstance(alerts, str)
|
||||
|
||||
def test_llmTool_get_google(self):
|
||||
from llm import llmTool_get_google
|
||||
result = llmTool_get_google("What is 2+2?", 1)
|
||||
self.assertIsInstance(result, list)
|
||||
|
||||
def test_send_ollama_query(self):
|
||||
from llm import send_ollama_query
|
||||
response = send_ollama_query("Hello, Ollama!")
|
||||
self.assertIsInstance(response, str)
|
||||
|
||||
def test_get_moon_phase(self):
|
||||
from space import get_moon
|
||||
phase = get_moon(lat, lon)
|
||||
self.assertIsInstance(phase, str)
|
||||
|
||||
def test_get_sun_times(self):
|
||||
from space import get_sun
|
||||
sun_times = get_sun(lat, lon)
|
||||
self.assertIsInstance(sun_times, str)
|
||||
|
||||
def test_hf_band_conditions(self):
|
||||
from space import hf_band_conditions
|
||||
conditions = hf_band_conditions()
|
||||
self.assertIsInstance(conditions, str)
|
||||
|
||||
def test_get_wikipedia_summary(self):
|
||||
from wiki import get_wikipedia_summary
|
||||
summary = get_wikipedia_summary("Python", location=(lat, lon))
|
||||
self.assertIsInstance(summary, str)
|
||||
|
||||
def test_get_kiwix_summary(self):
|
||||
from wiki import get_kiwix_summary
|
||||
summary = get_kiwix_summary("Python")
|
||||
self.assertIsInstance(summary, str)
|
||||
|
||||
def get_openskynetwork(self):
|
||||
from locationdata import get_openskynetwork
|
||||
flights = get_openskynetwork(lat, lon)
|
||||
self.assertIsInstance(flights, str)
|
||||
|
||||
def test_initalize_qrz_database(self):
|
||||
from qrz import initalize_qrz_database
|
||||
result = initalize_qrz_database()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_get_hamlib(self):
|
||||
from radio import get_hamlib
|
||||
frequency = get_hamlib('f')
|
||||
self.assertIsInstance(frequency, str)
|
||||
|
||||
def test_get_rss_feed(self):
|
||||
from rss import get_rss_feed
|
||||
result = get_rss_feed('')
|
||||
self.assertIsInstance(result, str)
|
||||
|
||||
|
||||
##### GAMES Tests #####
|
||||
def test_jokes(self):
|
||||
from modules.games.joke import tell_joke
|
||||
haha = tell_joke(nodeID=0, test=True)
|
||||
print("Joke response:", haha)
|
||||
self.assertIsInstance(haha, str)
|
||||
|
||||
def test_tictactoe_initial_and_move(self):
|
||||
from games.tictactoe import tictactoe
|
||||
user_id = "testuser"
|
||||
# Start a new game (no move yet)
|
||||
initial = tictactoe.play(user_id, "")
|
||||
print("Initial response:", initial)
|
||||
# Make a move, e.g., '1'
|
||||
second = tictactoe.play(user_id, "1")
|
||||
print("After move '1':", second)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(second, str)
|
||||
|
||||
def test_playVideoPoker(self):
|
||||
from games.videopoker import playVideoPoker
|
||||
user_id = "testuser"
|
||||
# Start a new game/session
|
||||
initial = playVideoPoker(user_id, 'deal')
|
||||
print("Initial response:", initial)
|
||||
# Place a 5-coin bet
|
||||
after_bet = playVideoPoker(user_id, '5')
|
||||
print("After placing 5-coin bet:", after_bet)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_bet, str)
|
||||
|
||||
def test_play_blackjack(self):
|
||||
from games.blackjack import playBlackJack
|
||||
user_id = "testuser"
|
||||
# Start a new game/session
|
||||
initial = playBlackJack(user_id, 'deal')
|
||||
print("Initial response:", initial)
|
||||
# Place a 5-chip bet
|
||||
after_bet = playBlackJack(user_id, '5')
|
||||
print("After placing 5-chip bet:", after_bet)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_bet, str)
|
||||
|
||||
|
||||
def test_hangman_initial_and_guess(self):
|
||||
from games.hangman import hangman
|
||||
user_id = "testuser"
|
||||
# Start a new game (no guess yet)
|
||||
initial = hangman.play(user_id, "")
|
||||
print("Initial response:", initial)
|
||||
# Guess a letter, e.g., 'e'
|
||||
second = hangman.play(user_id, "e")
|
||||
print("After guessing 'e':", second)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(second, str)
|
||||
|
||||
|
||||
def test_play_lemonade_stand(self):
|
||||
from games.lemonade import playLemonstand, lemonadeTracker
|
||||
user_id = "testuser"
|
||||
# Ensure user is in tracker
|
||||
if not any(u['nodeID'] == user_id for u in lemonadeTracker):
|
||||
lemonadeTracker.append({'nodeID': user_id, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': 30.0, 'start': 30.0, 'cmd': 'new', 'last_played': 0})
|
||||
# Start a new game
|
||||
initial = playLemonstand(user_id, "", newgame=True)
|
||||
print("Initial response:", initial)
|
||||
# Buy 1 box of cups
|
||||
after_cups = playLemonstand(user_id, "1")
|
||||
print("After buying 1 box of cups:", after_cups)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_cups, str)
|
||||
|
||||
|
||||
def test_play_golfsim_one_hole(self):
|
||||
from games.golfsim import playGolf
|
||||
user_id = "testuser"
|
||||
# Start a new game/hole
|
||||
initial = playGolf(user_id, "", last_cmd="new")
|
||||
print("Initial hole info:", initial)
|
||||
# Take first shot with driver
|
||||
after_shot = playGolf(user_id, "driver")
|
||||
print("After hitting driver:", after_shot)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_shot, str)
|
||||
|
||||
|
||||
def test_play_dopewar_choose_city_and_list(self):
|
||||
from games.dopewar import playDopeWars
|
||||
user_id = 1234567899 # Use a unique test user ID
|
||||
# Start a new game, get city selection prompt
|
||||
initial = playDopeWars(user_id, "")
|
||||
print("Initial city selection:", initial)
|
||||
# Choose city 1
|
||||
after_city = playDopeWars(user_id, "1")
|
||||
print("After choosing city 1 (main game list):", after_city)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_city, str)
|
||||
|
||||
|
||||
def test_play_mastermind_one_guess(self):
|
||||
from games.mmind import start_mMind
|
||||
user_id = 1234567899 # Use a unique test user ID
|
||||
# Start a new game (should prompt for difficulty/colors)
|
||||
initial = start_mMind(user_id, "n")
|
||||
print("Initial response (difficulty/colors):", initial)
|
||||
# Make a guess (e.g., "RGBY" - valid for normal)
|
||||
after_guess = start_mMind(user_id, "RGBY")
|
||||
print("After guessing RGBY:", after_guess)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(after_guess, str)
|
||||
|
||||
|
||||
def test_quiz_game_answer_one_and_end(self):
|
||||
from games.quiz import quizGamePlayer
|
||||
quizmaster_id = "admin" # Use a valid quizmaster ID from bbs_admin_list
|
||||
user_id = "testuser"
|
||||
# Start the quiz as quizmaster
|
||||
start_msg = quizGamePlayer.start_game(quizmaster_id)
|
||||
print("Quiz start:", start_msg)
|
||||
# User joins the quiz
|
||||
join_msg = quizGamePlayer.join(user_id)
|
||||
print("User joined:", join_msg)
|
||||
# Get the first question (should be included in join_msg, but call explicitly for clarity)
|
||||
question_msg = quizGamePlayer.next_question(user_id)
|
||||
print("First question:", question_msg)
|
||||
# Simulate answering with 'A' (adjust if your first question expects a different answer)
|
||||
answer_msg = quizGamePlayer.answer(user_id, "A")
|
||||
print("Answer response:", answer_msg)
|
||||
# End the quiz as quizmaster
|
||||
end_msg = quizGamePlayer.stop_game(quizmaster_id)
|
||||
print("Quiz end:", end_msg)
|
||||
self.assertIsInstance(start_msg, str)
|
||||
self.assertIsInstance(join_msg, str)
|
||||
self.assertIsInstance(question_msg, str)
|
||||
self.assertIsInstance(answer_msg, str)
|
||||
self.assertIsInstance(end_msg, str)
|
||||
|
||||
def test_survey_answer_one_and_end(self):
|
||||
from survey import survey_module
|
||||
user_id = "testuser"
|
||||
survey_name = "example" # Make sure this survey exists in your data/surveys directory
|
||||
|
||||
# Start the survey
|
||||
start_msg = survey_module.start_survey(user_id, survey_name)
|
||||
print("Survey start:", start_msg)
|
||||
# Answer the first question with 'A' (adjust if your survey expects a different type)
|
||||
answer_msg = survey_module.answer(user_id, "A")
|
||||
print("Answer response:", answer_msg)
|
||||
# End the survey
|
||||
end_msg = survey_module.end_survey(user_id)
|
||||
print("Survey end:", end_msg)
|
||||
|
||||
self.assertIsInstance(start_msg, str)
|
||||
self.assertIsInstance(answer_msg, str)
|
||||
self.assertIsInstance(end_msg, str)
|
||||
|
||||
|
||||
def test_hamtest_answer_one(self):
|
||||
from games.hamtest import hamtest
|
||||
user_id = "testuser"
|
||||
# Start a new ham test game (default level: technician)
|
||||
initial = hamtest.newGame(user_id)
|
||||
print("Initial question:", initial)
|
||||
# Answer the first question with 'A'
|
||||
answer_msg = hamtest.answer(user_id, "A")
|
||||
print("Answer response:", answer_msg)
|
||||
self.assertIsInstance(initial, str)
|
||||
self.assertIsInstance(answer_msg, str)
|
||||
|
||||
|
||||
##### API Tests - Extended tests run only if CHECKALL is True #####
|
||||
|
||||
|
||||
if CHECKALL:
|
||||
logger.info("Running extended API tests as CHECKALL is enabled.")
|
||||
def test_handledxcluster(self):
|
||||
from modules.dxspot import handledxcluster
|
||||
test_message = "DX band=20m mode=SSB of=K7MHI"
|
||||
response = handledxcluster(test_message, nodeID=0, deviceID='testdevice')
|
||||
print("DX Spotter response:", response)
|
||||
self.assertIsInstance(response, str)
|
||||
|
||||
def test_getRepeaterBook(self):
|
||||
from locationdata import getRepeaterBook
|
||||
repeaters = getRepeaterBook(lat, lon)
|
||||
self.assertIsInstance(repeaters, str)
|
||||
|
||||
def test_getArtSciRepeaters(self):
|
||||
from locationdata import getArtSciRepeaters
|
||||
repeaters = getArtSciRepeaters(lat, lon)
|
||||
self.assertIsInstance(repeaters, str)
|
||||
|
||||
def test_get_NOAAtides(self):
|
||||
from locationdata import get_NOAAtide
|
||||
tides = get_NOAAtide(lat, lon)
|
||||
self.assertIsInstance(tides, str)
|
||||
|
||||
def test_get_NOAAweather(self):
|
||||
from locationdata import get_NOAAweather
|
||||
weather = get_NOAAweather(lat, lon)
|
||||
self.assertIsInstance(weather, str)
|
||||
|
||||
def test_where_am_i(self):
|
||||
from locationdata import where_am_i
|
||||
location = where_am_i(lat, lon)
|
||||
self.assertIsInstance(location, str)
|
||||
|
||||
def test_getWeatherAlertsNOAA(self):
|
||||
from locationdata import getWeatherAlertsNOAA
|
||||
alerts = getWeatherAlertsNOAA(lat, lon)
|
||||
if isinstance(alerts, tuple):
|
||||
self.assertIsInstance(alerts[0], str)
|
||||
else:
|
||||
self.assertIsInstance(alerts, str)
|
||||
|
||||
def test_getActiveWeatherAlertsDetailNOAA(self):
|
||||
from locationdata import getActiveWeatherAlertsDetailNOAA
|
||||
alerts_detail = getActiveWeatherAlertsDetailNOAA(lat, lon)
|
||||
self.assertIsInstance(alerts_detail, str)
|
||||
|
||||
def test_getIpawsAlerts(self):
|
||||
from locationdata import getIpawsAlert
|
||||
alerts = getIpawsAlert(lat, lon)
|
||||
self.assertIsInstance(alerts, str)
|
||||
|
||||
def test_get_flood_noaa(self):
|
||||
from locationdata import get_flood_noaa
|
||||
flood_info = get_flood_noaa(lat, lon, 12484500) # Example gauge UID
|
||||
self.assertIsInstance(flood_info, str)
|
||||
|
||||
def test_get_volcano_usgs(self):
|
||||
from locationdata import get_volcano_usgs
|
||||
volcano_info = get_volcano_usgs(lat, lon)
|
||||
self.assertIsInstance(volcano_info, str)
|
||||
|
||||
def test_get_nws_marine_alerts(self):
|
||||
from locationdata import get_nws_marine
|
||||
marine_alerts = get_nws_marine('https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt',1) # Example zone
|
||||
self.assertIsInstance(marine_alerts, str)
|
||||
|
||||
def test_checkUSGSEarthQuakes(self):
|
||||
from locationdata import checkUSGSEarthQuake
|
||||
earthquakes = checkUSGSEarthQuake(lat, lon)
|
||||
self.assertIsInstance(earthquakes, str)
|
||||
|
||||
def test_getNextSatellitePass(self):
|
||||
from space import getNextSatellitePass
|
||||
pass_info = getNextSatellitePass('25544', lat, lon)
|
||||
self.assertIsInstance(pass_info, str)
|
||||
|
||||
def test_get_wx_meteo(self):
|
||||
from wx_meteo import get_wx_meteo
|
||||
weather_report = get_wx_meteo(lat, lon)
|
||||
self.assertIsInstance(weather_report, str)
|
||||
|
||||
def test_get_flood_openmeteo(self):
|
||||
from wx_meteo import get_flood_openmeteo
|
||||
flood_report = get_flood_openmeteo(lat, lon)
|
||||
self.assertIsInstance(flood_report, str)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not CHECKALL:
|
||||
print("\nNote: Extended API tests are skipped. To enable them, create a file named '.checkall' in the parent directory.\n")
|
||||
unittest.main()
|
||||
219
modules/wiki.py
219
modules/wiki.py
@@ -1,121 +1,126 @@
|
||||
# meshbot wiki module
|
||||
|
||||
from modules.log import *
|
||||
import wikipedia # pip install wikipedia
|
||||
from modules.log import logger
|
||||
from modules.settings import (use_kiwix_server, kiwix_url, kiwix_library_name,
|
||||
urlTimeoutSeconds, wiki_return_limit, ERROR_FETCHING_DATA)
|
||||
#import wikipedia # pip install wikipedia
|
||||
import requests
|
||||
import bs4 as bs
|
||||
from urllib.parse import quote
|
||||
|
||||
# Kiwix support for local wiki
|
||||
if use_kiwix_server:
|
||||
import requests
|
||||
import bs4 as bs
|
||||
from urllib.parse import quote
|
||||
def tag_visible(element):
|
||||
"""Filter visible text from HTML elements for Kiwix"""
|
||||
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
|
||||
return False
|
||||
if isinstance(element, bs.element.Comment):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Kiwix helper functions (only loaded if use_kiwix_server is True)
|
||||
if wikipedia_enabled and use_kiwix_server:
|
||||
def tag_visible(element):
|
||||
"""Filter visible text from HTML elements for Kiwix"""
|
||||
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
|
||||
return False
|
||||
if isinstance(element, bs.element.Comment):
|
||||
return False
|
||||
return True
|
||||
def text_from_html(body):
|
||||
"""Extract visible text from HTML content"""
|
||||
soup = bs.BeautifulSoup(body, 'html.parser')
|
||||
texts = soup.find_all(string=True)
|
||||
visible_texts = filter(tag_visible, texts)
|
||||
return " ".join(t.strip() for t in visible_texts if t.strip())
|
||||
|
||||
def text_from_html(body):
|
||||
"""Extract visible text from HTML content"""
|
||||
soup = bs.BeautifulSoup(body, 'html.parser')
|
||||
texts = soup.find_all(string=True)
|
||||
visible_texts = filter(tag_visible, texts)
|
||||
return " ".join(t.strip() for t in visible_texts if t.strip())
|
||||
def get_kiwix_summary(search_term):
|
||||
"""Query local Kiwix server for Wikipedia article"""
|
||||
if search_term is None or search_term.strip() == "":
|
||||
return ERROR_FETCHING_DATA
|
||||
try:
|
||||
search_encoded = quote(search_term)
|
||||
# Try direct article access first
|
||||
wiki_article = search_encoded.capitalize().replace("%20", "_")
|
||||
exact_url = f"{kiwix_url}/raw/{kiwix_library_name}/content/A/{wiki_article}"
|
||||
|
||||
response = requests.get(exact_url, timeout=urlTimeoutSeconds)
|
||||
if response.status_code == 200:
|
||||
# Extract and clean text
|
||||
text = text_from_html(response.text)
|
||||
# Remove common Wikipedia metadata prefixes
|
||||
text = text.split("Jump to navigation", 1)[-1]
|
||||
text = text.split("Jump to search", 1)[-1]
|
||||
# Truncate to reasonable length (first few sentences)
|
||||
sentences = text.split('. ')
|
||||
summary = '. '.join(sentences[:wiki_return_limit])
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '.'
|
||||
return summary.strip()[:500] # Hard limit at 500 chars
|
||||
|
||||
def get_kiwix_summary(search_term):
|
||||
"""Query local Kiwix server for Wikipedia article"""
|
||||
try:
|
||||
search_encoded = quote(search_term)
|
||||
# Try direct article access first
|
||||
wiki_article = search_encoded.capitalize().replace("%20", "_")
|
||||
exact_url = f"{kiwix_url}/raw/{kiwix_library_name}/content/A/{wiki_article}"
|
||||
# If direct access fails, try search
|
||||
logger.debug(f"System: Kiwix direct article not found for:{search_term} Status Code:{response.status_code}")
|
||||
search_url = f"{kiwix_url}/search?content={kiwix_library_name}&pattern={search_encoded}"
|
||||
response = requests.get(search_url, timeout=urlTimeoutSeconds)
|
||||
|
||||
if response.status_code == 200 and "No results were found" not in response.text:
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
links = [a['href'] for a in soup.find_all('a', href=True) if "start=" not in a['href']]
|
||||
|
||||
response = requests.get(exact_url, timeout=urlTimeoutSeconds)
|
||||
if response.status_code == 200:
|
||||
# Extract and clean text
|
||||
text = text_from_html(response.text)
|
||||
# Remove common Wikipedia metadata prefixes
|
||||
text = text.split("Jump to navigation", 1)[-1]
|
||||
text = text.split("Jump to search", 1)[-1]
|
||||
# Truncate to reasonable length (first few sentences)
|
||||
sentences = text.split('. ')
|
||||
summary = '. '.join(sentences[:wiki_return_limit])
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '.'
|
||||
return summary.strip()[:500] # Hard limit at 500 chars
|
||||
|
||||
# If direct access fails, try search
|
||||
search_url = f"{kiwix_url}/search?content={kiwix_library_name}&pattern={search_encoded}"
|
||||
response = requests.get(search_url, timeout=urlTimeoutSeconds)
|
||||
|
||||
if response.status_code == 200 and "No results were found" not in response.text:
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
links = [a['href'] for a in soup.find_all('a', href=True) if "start=" not in a['href']]
|
||||
|
||||
for link in links[:3]: # Check first 3 results
|
||||
article_name = link.split("/")[-1]
|
||||
if not article_name or article_name[0].islower():
|
||||
continue
|
||||
|
||||
article_url = f"{kiwix_url}{link}"
|
||||
article_response = requests.get(article_url, timeout=urlTimeoutSeconds)
|
||||
if article_response.status_code == 200:
|
||||
text = text_from_html(article_response.text)
|
||||
text = text.split("Jump to navigation", 1)[-1]
|
||||
text = text.split("Jump to search", 1)[-1]
|
||||
sentences = text.split('. ')
|
||||
summary = '. '.join(sentences[:wiki_return_limit])
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '.'
|
||||
return summary.strip()[:500]
|
||||
|
||||
logger.warning(f"System: No Kiwix Results for:{search_term}")
|
||||
# try to fall back to online Wikipedia if available
|
||||
return get_wikipedia_summary(search_term, force=True)
|
||||
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"System: Kiwix connection error: {e}")
|
||||
return "Unable to connect to local wiki server"
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error with Kiwix for:{search_term} {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
for link in links[:3]: # Check first 3 results
|
||||
article_name = link.split("/")[-1]
|
||||
if not article_name or article_name[0].islower():
|
||||
continue
|
||||
|
||||
article_url = f"{kiwix_url}{link}"
|
||||
article_response = requests.get(article_url, timeout=urlTimeoutSeconds)
|
||||
if article_response.status_code == 200:
|
||||
text = text_from_html(article_response.text)
|
||||
text = text.split("Jump to navigation", 1)[-1]
|
||||
text = text.split("Jump to search", 1)[-1]
|
||||
sentences = text.split('. ')
|
||||
summary = '. '.join(sentences[:wiki_return_limit])
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '.'
|
||||
return summary.strip()[:500]
|
||||
|
||||
logger.warning(f"System: No Kiwix Results for:{search_term}")
|
||||
# try to fall back to online Wikipedia if available
|
||||
return get_wikipedia_summary(search_term, force=True)
|
||||
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"System: Kiwix connection error: {e}")
|
||||
return "Unable to connect to local wiki server"
|
||||
# Fallback to online Wikipedia
|
||||
return get_wikipedia_summary(search_term, force=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error with Kiwix for:{search_term} {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def get_wikipedia_summary(search_term, location=None, force=False):
|
||||
lat, lon = location if location else (None, None)
|
||||
# Use Kiwix if configured
|
||||
if use_kiwix_server and not force:
|
||||
return get_kiwix_summary(search_term)
|
||||
|
||||
|
||||
if not search_term or not search_term.strip():
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
api_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{requests.utils.quote(search_term)}"
|
||||
headers = {
|
||||
"User-Agent": "MeshBot/1.0 (https://github.com/kkeeton/meshing-around; contact: youremail@example.com)"
|
||||
}
|
||||
try:
|
||||
# Otherwise use online Wikipedia
|
||||
wikipedia_search = wikipedia.search(search_term, results=3)
|
||||
wikipedia_suggest = wikipedia.suggest(search_term)
|
||||
#wikipedia_aroundme = wikipedia.geosearch(lat,lon, results=3)
|
||||
#logger.debug(f"System: Wikipedia Nearby:{wikipedia_aroundme}")
|
||||
response = requests.get(api_url, timeout=5, headers=headers)
|
||||
if response.status_code == 404:
|
||||
logger.warning(f"System: No Wikipedia Results for:{search_term}")
|
||||
return ERROR_FETCHING_DATA
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# Check for error response from Wikipedia API
|
||||
if "extract" not in data or not data.get("extract"):
|
||||
logger.warning(f"System: Wikipedia API returned no extract for:{search_term} (data: {data})")
|
||||
return ERROR_FETCHING_DATA
|
||||
summary = data.get("extract")
|
||||
if not summary or not isinstance(summary, str) or not summary.strip():
|
||||
logger.warning(f"System: No summary found for:{search_term}")
|
||||
return ERROR_FETCHING_DATA
|
||||
sentences = [s for s in summary.split('. ') if s.strip()]
|
||||
if not sentences:
|
||||
logger.warning(f"System: Wikipedia summary split produced no sentences for:{search_term}")
|
||||
return ERROR_FETCHING_DATA
|
||||
summary = '. '.join(sentences[:wiki_return_limit])
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '.'
|
||||
return summary.strip()[:500]
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Wikipedia search error for:{search_term} {e}")
|
||||
logger.warning(f"System: Wikipedia API error for:{search_term} {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
if len(wikipedia_search) == 0:
|
||||
logger.warning(f"System: No Wikipedia Results for:{search_term}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
try:
|
||||
logger.debug(f"System: Searching Wikipedia for:{search_term}, First Result:{wikipedia_search[0]}, Suggest Word:{wikipedia_suggest}")
|
||||
summary = wikipedia.summary(search_term, sentences=wiki_return_limit, auto_suggest=False, redirect=True)
|
||||
except wikipedia.DisambiguationError as e:
|
||||
logger.warning(f"System: Disambiguation Error for:{search_term} trying {wikipedia_search[0]}")
|
||||
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
|
||||
except wikipedia.PageError as e:
|
||||
logger.warning(f"System: Wikipedia Page Error for:{search_term} {e} trying {wikipedia_search[0]}")
|
||||
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error with Wikipedia for:{search_term} {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
return summary
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import requests
|
||||
import json
|
||||
from modules.log import *
|
||||
from modules.log import logger
|
||||
from modules.settings import ERROR_FETCHING_DATA
|
||||
|
||||
def get_weather_data(api_url, params):
|
||||
response = requests.get(api_url, params=params)
|
||||
|
||||
233
pong_bot.py
233
pong_bot.py
@@ -12,7 +12,8 @@ import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from datetime import datetime
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.log import logger, CustomFormatter, msgLogger
|
||||
import modules.settings as my_settings
|
||||
from modules.system import *
|
||||
|
||||
# Global Variables
|
||||
@@ -134,13 +135,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
elif not my_settings.autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
except ValueError:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
@@ -151,7 +152,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not useDMForResponse and not isDM:
|
||||
if not my_settings.useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
@@ -161,8 +162,8 @@ def handle_motd(message, message_from_id, isDM):
|
||||
isAdmin = False
|
||||
msg = MOTD
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
if my_settings.bbs_admin_list != ['']:
|
||||
for admin in my_settings.bbs_admin_list:
|
||||
if str(message_from_id) == admin:
|
||||
isAdmin = True
|
||||
break
|
||||
@@ -191,7 +192,7 @@ def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
|
||||
parts = message.lower().split("echo ", 1)
|
||||
if len(parts) > 1 and parts[1].strip() != "":
|
||||
echo_msg = parts[1]
|
||||
if channel_number != echoChannel:
|
||||
if channel_number != my_settings.echoChannel:
|
||||
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
|
||||
return echo_msg
|
||||
else:
|
||||
@@ -238,7 +239,8 @@ def onReceive(packet, interface):
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
for item in interface.__dict__.items(): intDebug = f"{item}\n"
|
||||
for item in interface.__dict__.items():
|
||||
intDebug = f"{item}\n"
|
||||
logger.debug(f"System: Packet Received on {rxType} Interface\n {intDebug} \n END of interface \n")
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
@@ -373,7 +375,7 @@ def onReceive(packet, interface):
|
||||
if hop in ("MQTT", "Gateway") and hop_count > 0:
|
||||
hop = f"{hop_count} Hops"
|
||||
|
||||
if enableHopLogs:
|
||||
if my_settings.enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
@@ -417,7 +419,7 @@ def onReceive(packet, interface):
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
if channel_number == my_settings.publicChannel and my_settings.antiSpam:
|
||||
# warning user spamming default channel
|
||||
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
@@ -430,7 +432,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# message is not for bot to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
if my_settings.zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
|
||||
@@ -448,17 +450,17 @@ def onReceive(packet, interface):
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and multiple_interface:
|
||||
if my_settings.repeater_enabled and multiple_interface:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
time.sleep(my_settings.responseDelay)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
if str(channel_number) in my_settings.repeater_channels:
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
|
||||
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, i)
|
||||
time.sleep(responseDelay)
|
||||
time.sleep(my_settings.responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode, channel_number)
|
||||
@@ -467,66 +469,185 @@ def onReceive(packet, interface):
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
async def start_rx():
|
||||
print (CustomFormatter.bold_white + "\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False):
|
||||
myNodeNum = globals().get(f'myNodeNum{i}', 0)
|
||||
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
|
||||
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
|
||||
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
logger.debug("System: Logging System Logs to disk")
|
||||
if motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if enableEcho:
|
||||
logger.debug(f"System: Echo command Enabled")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
|
||||
if repeater_enabled and multiple_interface:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if bbs_enabled:
|
||||
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
|
||||
if bbs_link_enabled:
|
||||
if len(bbs_link_whitelist) > 0:
|
||||
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
|
||||
else:
|
||||
logger.debug(f"System: BBS Link Enabled allowing all")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
logger.debug("System: RX Subscriber started")
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
await asyncio.sleep(0.5)
|
||||
pass
|
||||
|
||||
def handle_boot(mesh=True):
|
||||
try:
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
if mesh:
|
||||
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False):
|
||||
myNodeNum = globals().get(f'myNodeNum{i}', 0)
|
||||
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
|
||||
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
|
||||
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model {my_settings.llmModel} please wait")
|
||||
llmLoad = llm_query(" ")
|
||||
if "trouble" not in llmLoad:
|
||||
logger.debug(f"System: LLM Model {my_settings.llmModel} loaded")
|
||||
|
||||
if my_settings.bbs_enabled:
|
||||
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
|
||||
if my_settings.bbs_link_enabled:
|
||||
if len(bbs_link_whitelist) > 0:
|
||||
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
|
||||
else:
|
||||
logger.debug(f"System: BBS Link Enabled allowing all")
|
||||
|
||||
if my_settings.solar_conditions_enabled:
|
||||
logger.debug("System: Celestial Telemetry Enabled")
|
||||
|
||||
if my_settings.location_enabled:
|
||||
if my_settings.use_meteo_wxApi:
|
||||
logger.debug("System: Location Telemetry Enabled using Open-Meteo API")
|
||||
else:
|
||||
logger.debug("System: Location Telemetry Enabled using NOAA API")
|
||||
print("debug my_settings.scheduler_enabled:", my_settings.scheduler_enabled)
|
||||
if my_settings.dad_jokes_enabled:
|
||||
logger.debug("System: Dad Jokes Enabled!")
|
||||
|
||||
if my_settings.coastalEnabled:
|
||||
logger.debug("System: Coastal Forecast and Tide Enabled!")
|
||||
|
||||
if games_enabled:
|
||||
logger.debug("System: Games Enabled!")
|
||||
|
||||
if my_settings.wikipedia_enabled:
|
||||
if my_settings.use_kiwix_server:
|
||||
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_url}")
|
||||
else:
|
||||
logger.debug("System: Wikipedia search Enabled")
|
||||
|
||||
if my_settings.rssEnable:
|
||||
logger.debug(f"System: RSS Feed Reader Enabled for feeds: {rssFeedNames}")
|
||||
|
||||
if my_settings.radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {my_settings.rigControlServerAddress} broadcasting to channels: {my_settings.sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
|
||||
if my_settings.file_monitor_enabled:
|
||||
logger.warning(f"System: File Monitor Enabled for {my_settings.file_monitor_file_path}, broadcasting to channels: {my_settings.file_monitor_broadcastCh}")
|
||||
if my_settings.enable_runShellCmd:
|
||||
logger.debug("System: Shell Command monitor enabled")
|
||||
if my_settings.allowXcmd:
|
||||
logger.warning("System: File Monitor shell XCMD Enabled")
|
||||
if my_settings.read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {my_settings.news_file_path}")
|
||||
if my_settings.bee_enabled:
|
||||
logger.debug("System: File Monitor Bee Monitor Enabled for bee.txt")
|
||||
|
||||
if my_settings.wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {my_settings.wxAlertBroadcastChannel}")
|
||||
|
||||
if my_settings.emergencyAlertBrodcastEnabled:
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {my_settings.emergencyAlertBroadcastCh} for FIPS codes {my_settings.myStateFIPSList}")
|
||||
if my_settings.myStateFIPSList == ['']:
|
||||
logger.warning("System: No FIPS codes set for iPAWS Alerts")
|
||||
|
||||
if my_settings.emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {my_settings.emergency_responder_alert_channel} for interface {my_settings.emergency_responder_alert_interface}")
|
||||
|
||||
if my_settings.volcanoAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Volcano Alert Broadcast Enabled on channels {my_settings.volcanoAlertBroadcastChannel}")
|
||||
|
||||
if my_settings.qrz_hello_enabled:
|
||||
if my_settings.train_qrz:
|
||||
logger.debug("System: QRZ Welcome/Hello Enabled with training mode")
|
||||
else:
|
||||
logger.debug("System: QRZ Welcome/Hello Enabled")
|
||||
|
||||
if my_settings.enableSMTP:
|
||||
if my_settings.enableImap:
|
||||
logger.debug("System: SMTP Email Alerting Enabled using IMAP")
|
||||
else:
|
||||
logger.warning("System: SMTP Email Alerting Enabled")
|
||||
|
||||
# Default Options
|
||||
if my_settings.useDMForResponse:
|
||||
logger.debug("System: Respond by DM only")
|
||||
|
||||
if my_settings.log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if my_settings.syslog_to_file:
|
||||
logger.debug("System: Logging System Logs to disk")
|
||||
|
||||
if my_settings.motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {my_settings.MOTD} scheduler:{my_settings.schedulerMotd}")
|
||||
|
||||
if my_settings.sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {my_settings.sentry_radius}m radius reporting to channel:{my_settings.secure_channel} requestLOC:{reqLocationEnabled}")
|
||||
if my_settings.sentryIgnoreList:
|
||||
logger.debug(f"System: Sentry BlockList Enabled for nodes: {my_settings.sentryIgnoreList}")
|
||||
if my_settings.sentryWatchList:
|
||||
logger.debug(f"System: Sentry WatchList Enabled for nodes: {my_settings.sentryWatchList}")
|
||||
|
||||
if my_settings.highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {my_settings.highfly_altitude}m limit reporting to channel:{my_settings.highfly_channel}")
|
||||
|
||||
if my_settings.store_forward_enabled:
|
||||
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit} and reverse queue:{my_settings.reverseSF}")
|
||||
|
||||
if my_settings.enableEcho:
|
||||
logger.debug("System: Echo command Enabled")
|
||||
|
||||
if my_settings.repeater_enabled and multiple_interface:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {my_settings.repeater_channels}")
|
||||
|
||||
if my_settings.checklist_enabled:
|
||||
logger.debug("System: CheckList Module Enabled")
|
||||
|
||||
if my_settings.ignoreChannels:
|
||||
logger.debug(f"System: Ignoring Channels: {my_settings.ignoreChannels}")
|
||||
|
||||
if my_settings.noisyNodeLogging:
|
||||
logger.debug("System: Noisy Node Logging Enabled")
|
||||
|
||||
if my_settings.logMetaStats:
|
||||
logger.debug("System: Logging Metadata Stats Enabled, leaderboard")
|
||||
|
||||
if my_settings.scheduler_enabled:
|
||||
logger.debug("System: Scheduler Enabled")
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error during boot: {e}")
|
||||
|
||||
|
||||
# Hello World
|
||||
async def main():
|
||||
tasks = []
|
||||
|
||||
try:
|
||||
handle_boot(mesh=False) # pong bot
|
||||
# Create core tasks
|
||||
tasks.append(asyncio.create_task(start_rx(), name="pong_rx"))
|
||||
tasks.append(asyncio.create_task(start_rx(), name="mesh_rx"))
|
||||
tasks.append(asyncio.create_task(watchdog(), name="watchdog"))
|
||||
|
||||
# Add optional tasks
|
||||
if file_monitor_enabled:
|
||||
if my_settings.file_monitor_enabled:
|
||||
tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor"))
|
||||
|
||||
if my_settings.radio_detection_enabled:
|
||||
tasks.append(asyncio.create_task(handleSignalWatcher(), name="hamlib"))
|
||||
|
||||
if my_settings.voxDetectionEnabled:
|
||||
tasks.append(asyncio.create_task(voxMonitor(), name="vox_detection"))
|
||||
|
||||
if my_settings.scheduler_enabled:
|
||||
from modules.scheduler import run_scheduler_loop, setup_scheduler
|
||||
setup_scheduler(schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval)
|
||||
tasks.append(asyncio.create_task(run_scheduler_loop(), name="scheduler"))
|
||||
|
||||
logger.debug(f"System: Starting {len(tasks)} async tasks")
|
||||
|
||||
# Wait for all tasks with proper exception handling
|
||||
|
||||
@@ -8,5 +8,4 @@ beautifulsoup4
|
||||
dadjokes
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
googlesearch-python
|
||||
|
||||
@@ -34,8 +34,10 @@ print("---------------------------------------------------------------")
|
||||
try:
|
||||
# set the path to import the modules and config.ini
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
from modules.log import logger, getPrettyTime
|
||||
from modules.system import handleFavoriteNode
|
||||
from modules.settings import LOGGING_LEVEL
|
||||
from modules.system import compileFavoriteList
|
||||
except Exception as e:
|
||||
print(f"Error importing modules run this program from the main repo directory 'python3 script/addFav.py'")
|
||||
print(f"if you forgot the rest of it.. git clone https://github.com/spudgunman/meshing-around")
|
||||
|
||||
@@ -14,8 +14,10 @@ print("---------------------------------------------------------------")
|
||||
try:
|
||||
# set the path to import the modules and config.ini
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from modules.log import *
|
||||
from modules.bbstools import *
|
||||
from modules.log import logger
|
||||
from modules.bbstools import bbs_post_dm, bbs_dm, get_bbs_stats
|
||||
from modules.settings import LOGGING_LEVEL, vpTracker, MOTD
|
||||
logger.setLevel(LOGGING_LEVEL)
|
||||
except Exception as e:
|
||||
print(f"Error importing modules run this program from the main program directory 'python3 script/injectDM.py'")
|
||||
exit(1)
|
||||
|
||||
15
update.sh
15
update.sh
@@ -3,6 +3,7 @@
|
||||
# Usage: bash update.sh or ./update.sh after making it executable with chmod +x update.sh
|
||||
|
||||
# Check if the mesh_bot.service or pong_bot.service
|
||||
service_stopped=false
|
||||
if systemctl is-active --quiet mesh_bot.service; then
|
||||
echo "Stopping mesh_bot.service..."
|
||||
systemctl stop mesh_bot.service
|
||||
@@ -48,6 +49,8 @@ fi
|
||||
if [[ ! -f modules/custom_scheduler.py ]]; then
|
||||
cp -n etc/custom_scheduler.py modules/
|
||||
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
|
||||
elif ! cmp -s modules/custom_scheduler.py etc/custom_scheduler.py; then
|
||||
echo "custom_scheduler.py is set. To check changes run: diff etc/custom_scheduler.py modules/custom_scheduler.py"
|
||||
fi
|
||||
|
||||
# Backup the data/ directory
|
||||
@@ -81,14 +84,14 @@ else
|
||||
echo "Configuration merge log (ini_merge_log.txt) not found. check out the script/configMerge.py tool!"
|
||||
fi
|
||||
|
||||
# if service was stopped earlier, restart it
|
||||
if [[ "$service_stopped" = true ]]; then
|
||||
echo "Restarting services..."
|
||||
systemctl start mesh_bot.service
|
||||
systemctl start pong_bot.service
|
||||
systemctl start mesh_bot_reporting.service
|
||||
systemctl start mesh_bot_w3.service
|
||||
echo "Services restarted."
|
||||
for svc in mesh_bot.service pong_bot.service mesh_bot_reporting.service mesh_bot_w3.service; do
|
||||
if systemctl list-unit-files | grep -q "^$svc"; then
|
||||
systemctl start "$svc"
|
||||
echo "$svc restarted."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Print completion message
|
||||
|
||||
Reference in New Issue
Block a user