Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0aeb2a2e | ||
|
|
2476fa9fae | ||
|
|
bc4854e6f4 | ||
|
|
6362f2558e | ||
|
|
a750628097 | ||
|
|
ce31a2e5df | ||
|
|
570e9d14a6 | ||
|
|
23fe33e3ad | ||
|
|
54c16d81dc | ||
|
|
04d7d5c37f | ||
|
|
fbc3ceb3c4 | ||
|
|
e1703ae326 | ||
|
|
65a1fe413d | ||
|
|
451938772a | ||
|
|
591d44904e | ||
|
|
84dda24e9d | ||
|
|
ebb9865109 | ||
|
|
e6cf3c6984 | ||
|
|
01e102af17 | ||
|
|
790c224dcc | ||
|
|
34b80aa274 | ||
|
|
a35be2f669 | ||
|
|
06c0f98e25 | ||
|
|
14104b4f39 | ||
|
|
1184102860 | ||
|
|
4e3d735b1d | ||
|
|
b074306696 | ||
|
|
0d5fd6c0cf | ||
|
|
8647451b24 | ||
|
|
c89bfaeff1 | ||
|
|
74f6e0a32c | ||
|
|
be9173a7ff | ||
|
|
1666e8fbf9 | ||
|
|
b950886c2d | ||
|
|
9a627c8a83 | ||
|
|
9f11775db8 | ||
|
|
d994b9f8fe | ||
|
|
a7e5b3bea2 | ||
|
|
ed2e00ac3c | ||
|
|
0fd568fd87 | ||
|
|
2801e1559f | ||
|
|
3007aa56f5 | ||
|
|
669333e2c7 | ||
|
|
bf88d3d7c1 | ||
|
|
3751a29dfb | ||
|
|
2aae88ed22 | ||
|
|
dc2ea9a7e3 | ||
|
|
7285c4da85 | ||
|
|
e0c34659a2 | ||
|
|
32bb958ac0 | ||
|
|
70b721b40f | ||
|
|
a80f62f8d4 | ||
|
|
a9efb888bd | ||
|
|
59fdac5803 | ||
|
|
64173df84d | ||
|
|
5859bbdac8 | ||
|
|
5b6555b8da | ||
|
|
0e287c369d | ||
|
|
98f7ced3bf | ||
|
|
dce4303ca1 | ||
|
|
1153d1919c | ||
|
|
499d86459b | ||
|
|
c053b58733 | ||
|
|
e018cfa05b | ||
|
|
9f58177559 | ||
|
|
58e1781397 | ||
|
|
f08eb06db4 | ||
|
|
807bf17f61 | ||
|
|
3e354d3655 | ||
|
|
fa38734869 | ||
|
|
66ed4fd4d1 | ||
|
|
c528e0f638 | ||
|
|
41b1c73ff3 | ||
|
|
b1d1a7b1cc | ||
|
|
8bb132be24 | ||
|
|
15d0a01dcb | ||
|
|
ad05b2f427 | ||
|
|
2cbaef99c0 | ||
|
|
a53cab8343 | ||
|
|
e00da20870 | ||
|
|
45d8d5702c | ||
|
|
86e7944837 | ||
|
|
e9a2165f78 | ||
|
|
9421b22cff | ||
|
|
5767d2d606 | ||
|
|
3b5ecf4b6b | ||
|
|
d961698d40 | ||
|
|
37e9512d82 | ||
|
|
5e1164d98c | ||
|
|
1e64e992dd | ||
|
|
667126c916 | ||
|
|
5ada6cfb36 | ||
|
|
b6d5ce6836 | ||
|
|
0077f1a80b | ||
|
|
dfa82dbe96 | ||
|
|
ba014f4c98 | ||
|
|
6823161897 | ||
|
|
fcb0051c87 | ||
|
|
403aa9a362 | ||
|
|
8d91336b43 | ||
|
|
25abfb2a59 | ||
|
|
3f9a976742 | ||
|
|
8f41991a3d | ||
|
|
1785162cea | ||
|
|
0207424d32 | ||
|
|
ab7009e43f | ||
|
|
5c0e61d466 | ||
|
|
2cabbbf6f1 | ||
|
|
803e8d552d | ||
|
|
0950092f6d | ||
|
|
f6e15d9c35 | ||
|
|
5b917e032f | ||
|
|
7628f8d1ae | ||
|
|
1e496efbee | ||
|
|
0cf348570b | ||
|
|
55419dbafb | ||
|
|
cf3757ce11 | ||
|
|
4cf45b2684 | ||
|
|
544f7b5833 | ||
|
|
fa2a34c951 | ||
|
|
affbf366a1 | ||
|
|
4056f60cd8 | ||
|
|
a7a36f123c | ||
|
|
6865041707 | ||
|
|
520758e2f2 | ||
|
|
cd74624747 | ||
|
|
525f13b3e8 | ||
|
|
c82dc32ca2 | ||
|
|
ccb260c3f9 | ||
|
|
fa9fb6b27d | ||
|
|
b222b2342e | ||
|
|
a0d5c63aee | ||
|
|
0976c2c9e3 | ||
|
|
82647b2597 | ||
|
|
e20ac2c849 | ||
|
|
37a689e58e | ||
|
|
79043feb91 | ||
|
|
c2f905229f | ||
|
|
114e6aeb9b | ||
|
|
83f7270d6b | ||
|
|
63602bc19c | ||
|
|
65a608395b | ||
|
|
f11c076258 | ||
|
|
7b7f57f4c0 | ||
|
|
e2c100c5bb | ||
|
|
57e5c37f1a | ||
|
|
d53cc3b3f6 | ||
|
|
fc7ef5eb69 | ||
|
|
ccba129bb2 | ||
|
|
d9f52bdafd | ||
|
|
f0615c51e8 | ||
|
|
6deb4455ed | ||
|
|
86d7e414c2 | ||
|
|
19304b5d27 | ||
|
|
f87a07f6bd | ||
|
|
6b3490557c | ||
|
|
006468bd35 | ||
|
|
2cf516dd1f | ||
|
|
0467fde122 | ||
|
|
d9362fb819 | ||
|
|
b165e67676 | ||
|
|
1dc54fa5f2 | ||
|
|
91063db833 | ||
|
|
c9f2d07f4b | ||
|
|
290bdc3903 | ||
|
|
93734ec773 | ||
|
|
37a6b08fe2 | ||
|
|
f1e35bae27 | ||
|
|
5c2b426748 | ||
|
|
ed6606f8ac | ||
|
|
be4b46a81d | ||
|
|
f0c65454d3 | ||
|
|
646081286f | ||
|
|
e36d8727c5 | ||
|
|
e8cf0f2c24 | ||
|
|
cd4b3bbb38 | ||
|
|
2a5ced954b | ||
|
|
ea2a76bf81 | ||
|
|
1be2590f43 | ||
|
|
f37fc95ddc | ||
|
|
2515eb83a5 | ||
|
|
47e5c364a9 | ||
|
|
a03efbd2a9 | ||
|
|
d49d368895 | ||
|
|
ba792ff1f5 | ||
|
|
faad238f8c | ||
|
|
cc1368258c | ||
|
|
7c1b3c207d | ||
|
|
8f9af7d10c | ||
|
|
4ec07fdf77 | ||
|
|
7e9be22734 | ||
|
|
d85149e5f1 | ||
|
|
0e393b089f | ||
|
|
c16fb46b78 | ||
|
|
f051fe0087 | ||
|
|
bf2ac43a88 | ||
|
|
87f82af697 | ||
|
|
9036557154 | ||
|
|
c9c67f32c9 | ||
|
|
cc90375376 | ||
|
|
6df3f8a55f | ||
|
|
0bf0134a86 | ||
|
|
576f21b6fc | ||
|
|
824c952ad9 | ||
|
|
eeae88595d | ||
|
|
a709f25ea3 | ||
|
|
4c43ab0e8e | ||
|
|
d97a2d1fe3 | ||
|
|
10baa139a1 | ||
|
|
be04db6237 | ||
|
|
ce319f8696 | ||
|
|
b26139fdac | ||
|
|
98cf67381b | ||
|
|
132267f5d4 | ||
|
|
1a204d303b | ||
|
|
f82227ea0d | ||
|
|
238bb9f5ea | ||
|
|
f026c88817 | ||
|
|
fa2fb34e41 | ||
|
|
85cf5f5ce5 | ||
|
|
1e62920925 | ||
|
|
d58c7245e7 | ||
|
|
861397b455 | ||
|
|
a265de4a62 | ||
|
|
d9883b6f97 | ||
|
|
64d80c79ac | ||
|
|
91def4cd4d | ||
|
|
6a46983293 | ||
|
|
0544d870e7 | ||
|
|
e1327961b6 | ||
|
|
153bc39e63 | ||
|
|
f94f69aa59 | ||
|
|
9dfae705e5 | ||
|
|
32144c5657 | ||
|
|
5235df2259 | ||
|
|
eca6fae897 | ||
|
|
1e58d5017d | ||
|
|
75121c5a79 | ||
|
|
71231d3a9b | ||
|
|
3b6c82d6f5 | ||
|
|
1fc96f6b3a | ||
|
|
c7216563e6 | ||
|
|
4a27acb4ac | ||
|
|
e0d77bf82d | ||
|
|
84e6f8ed6d | ||
|
|
67309f5925 | ||
|
|
8480215db9 | ||
|
|
7bab9a1238 | ||
|
|
e08621d317 | ||
|
|
204acac19c | ||
|
|
077e82bda2 | ||
|
|
9af4a52927 | ||
|
|
2970adc634 | ||
|
|
a95b88d99f | ||
|
|
3832d13200 | ||
|
|
ca06fea13f | ||
|
|
5575dbb054 | ||
|
|
8d02152697 | ||
|
|
ae601c45f5 | ||
|
|
7391e68778 | ||
|
|
0f607f67ee | ||
|
|
142a7fcf48 | ||
|
|
2ef6cf6053 | ||
|
|
dd0dbb0f27 | ||
|
|
f85a637566 | ||
|
|
bb711b8167 | ||
|
|
9f4ee76bb7 | ||
|
|
4464d589d9 | ||
|
|
de815af682 | ||
|
|
46dc4190a0 | ||
|
|
c8414ba000 | ||
|
|
9eaf1d8029 | ||
|
|
b26af574ae | ||
|
|
51692a17db | ||
|
|
b90b5fc8f4 | ||
|
|
6e0642a11b | ||
|
|
fba8cc680c | ||
|
|
1547784a40 | ||
|
|
3b65435899 | ||
|
|
bd98d1bdcb | ||
|
|
d946067bd4 | ||
|
|
d15267ea53 | ||
|
|
05b7520a3f | ||
|
|
f8e04ea086 | ||
|
|
e594d27beb | ||
|
|
427937e8d5 | ||
|
|
ff189b7357 | ||
|
|
92542b4405 | ||
|
|
351fb9c090 | ||
|
|
af6862dfe5 | ||
|
|
4e562c5c66 | ||
|
|
25d0aad3cb | ||
|
|
82743a0d66 | ||
|
|
ce51483344 | ||
|
|
40806125a5 | ||
|
|
bb33e9f82b | ||
|
|
7fdbcdf951 | ||
|
|
369b3bf867 | ||
|
|
b62f864167 | ||
|
|
5b748dc207 | ||
|
|
cbf96086d3 | ||
|
|
29b7cd93da | ||
|
|
7987fe186a | ||
|
|
3fd8c5de0e | ||
|
|
82fda0be94 | ||
|
|
3f9cbf8964 | ||
|
|
d86f634827 | ||
|
|
82b08ce80b | ||
|
|
7015ac4f7e | ||
|
|
bf8a306870 | ||
|
|
2a3fc39da6 | ||
|
|
079770da7b | ||
|
|
cabf2d3ba8 | ||
|
|
9f8864683d | ||
|
|
56685916f0 | ||
|
|
991e0f2429 | ||
|
|
89235209e1 | ||
|
|
66ff3375ff | ||
|
|
993adfc196 | ||
|
|
fdbe058c2a | ||
|
|
4471e5930d | ||
|
|
1c7c739942 | ||
|
|
0a7eec666d | ||
|
|
41c640ca10 | ||
|
|
2e961db66f | ||
|
|
c0d817fb69 | ||
|
|
6d1b82d3c3 | ||
|
|
8805587576 | ||
|
|
a8e62c5ce9 |
No files matched your search
@@ -1 +1,3 @@
|
||||
.vscode/launch.json
|
||||
__pycache__
|
||||
bot_config.conf
|
||||
@@ -2,18 +2,61 @@
|
||||
|
||||
[](http://gitlab.rickelobe.com/Bots/r0b3BOT/commits/master)
|
||||
|
||||
# Screenshot
|
||||

|
||||
|
||||
# Description
|
||||
This is R0b3's Discord Bot. It's creation was basically a challenge posed a coworker who said "Hey, you can write Discord Bots in Python... Make one to display your 3D printer status!".
|
||||
|
||||
# Features
|
||||
* Query & Monitor OctoPrint 3D Printer Status using the `$printstat` command (Requires that your printer is connected to [Octoprint](https://octoprint.org/))<br>
|
||||

|
||||
* Query & Monitor Bambu Lab Printer Status using the `$bambustat` command (Requires a Bambu Lab printer accessible on the local network via MQTT)<br>
|
||||
* Displays print state, progress percentage, time remaining, current layer, bed temp, nozzle temp, and chamber temp
|
||||
* Optionally captures and attaches a camera snapshot from the printer's RTSPS stream (requires [ffmpeg](https://ffmpeg.org/) on the host)
|
||||
* Updates the bot's Discord status to show live Bambu print progress while a print is running
|
||||
* Play a selection of sound clips to voice channel members using the various commands (`$torololo`, `$oops`, `$promoted`, etc)
|
||||
|
||||
# Commands
|
||||

|
||||
|
||||
# Requirements
|
||||
1. discord.py version 1.2.4 or greater<br>
|
||||
<code> pip install discord.py </code>
|
||||
|
||||
Install all Python dependencies using the provided `requirements.txt`:
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Dependencies:
|
||||
- `discord.py[voice]` >= 2.6.4 — Discord bot framework (voice support included)
|
||||
- `requests` >= 2.32.3 — HTTP client for HomeAssistant REST API calls
|
||||
- `bambu-connect` >= 0.3.1 — Bambu Lab printer integration via local MQTT
|
||||
- `paho-mqtt` >= 2.0 — MQTT client library (required by bambu-connect)
|
||||
|
||||
**Optional: Camera snapshot support**
|
||||
|
||||
To enable camera snapshots with `$bambustat`, [ffmpeg](https://ffmpeg.org/) must be installed on the host running the bot. Set `bambu_camera_enable = true` in `bot_config.conf` once ffmpeg is available.
|
||||
|
||||
# Configuration
|
||||
1. Create a Discord Bot via Developer tools<br>
|
||||
2. Enter various configuration parameters in bot_config.conf.example<br>
|
||||
3. Enter OctoPrint config parameters in octoapi.conf.example<br>
|
||||
4. Rename *.example files as *.conf<br>
|
||||
1. Create a Discord Bot via the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Copy `bot_config.conf.example` to `bot_config.conf` and fill in your values (see below)
|
||||
3. Rename `octoapi.conf.example` to `octoapi.conf` and configure OctoPrint settings (if using OctoPrint)
|
||||
|
||||
## bot_config.conf Keys
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `discord_auth_token` | Bot token from the Discord Developer Console |
|
||||
| `bot_command_prefix` | Command prefix (e.g. `$`, `!`, `/`) |
|
||||
| `hass_url` | URL to your Home Assistant instance |
|
||||
| `hass_token` | Long-lived access token for Home Assistant REST API |
|
||||
| `hass_light` | Entity ID of the light at your printer |
|
||||
| `octoprint_ip_address` | IP address of your OctoPrint/OctoPi host |
|
||||
| `bambu_ip_address` | IP address of your Bambu Lab printer on the local network |
|
||||
| `bambu_access_code` | Access code shown on the printer screen (Settings > WLAN) |
|
||||
| `bambu_serial_number` | Serial number of your Bambu Lab printer |
|
||||
| `bambu_camera_enable` | Set to `true` to capture camera snapshots with `$bambustat` (requires ffmpeg) |
|
||||
| `statping_enable` | Set to `true` to enable StatPing service status integration |
|
||||
| `statping_url` | URL of your StatPing instance |
|
||||
| `statping_api_key` | API key for StatPing |
|
||||
| `mmr_checker_enable` | Set to `true` to enable MMR checker |
|
||||
| `mmr_checker_interval` | Seconds between MMR checks |
|
||||
| `mmr_api_url` | MMR API URL |
|
||||
@@ -0,0 +1,25 @@
|
||||
[bot-config]
|
||||
#
|
||||
# Replace the placeholders with your information. Do not use quotes.
|
||||
#
|
||||
# discord_auth_token - this is the bot token generated via the Discord development console.
|
||||
# bot_command_prefix - The prefix used identify commands for this bot
|
||||
# hass_url - URL to your home assistant instance. This is used to turn on the light by your 3d printer
|
||||
# hass_token - Long Lived Access Token used to authenticate with the REST API for your Home assistant instance
|
||||
# hass_light - Entity name of the light you want to control
|
||||
# octoprint_ip_address - IP Address of you OctoPrint (or OctoPi) host. Used to get 3D printer stats.
|
||||
# statping_url - Server hostname of the StatPing instance we will be querying w/ the REST API
|
||||
# statping_api_key - API Key for Statping to query for service status
|
||||
#
|
||||
discord_auth_token = Njk4NTY1MDM5MzAwNDExNDIy.XpHrZA.QCXjDj_Sk36Ohtu8ryXKSoGKMkc
|
||||
bot_command_prefix = #
|
||||
hass_url = <URL to HomeAssistant>
|
||||
hass_token = <HomeAssistant Long Lived Access Token>
|
||||
hass_light = <Entity_ID of the light @ your printer>
|
||||
octoprint_ip_address = <OctoPrint IP Address>
|
||||
statping_enable = False
|
||||
statping_url = <Statping URL>
|
||||
statping_api_key = <Statping API Key>
|
||||
mmr_checker_enable = True
|
||||
mmr_checker_interval = 600
|
||||
mmr_api_url = https://na.whatismymmr.com/api/v1/summoner?name=
|
||||
+20
-2
@@ -3,14 +3,32 @@
|
||||
# Replace the placeholders with your information. Do not use quotes.
|
||||
#
|
||||
# discord_auth_token - this is the bot token generated via the Discord development console.
|
||||
# bot_command_prefix - The prefix used identify commands for this bot
|
||||
# hass_url - URL to your home assistant instance. This is used to turn on the light by your 3d printer
|
||||
# hass_token - Long Lived Access Token used to authenticate with the REST API for your Home assistant instance
|
||||
# hass_light - Entity name of the light you want to control
|
||||
# octoprint_ip_address - IP Address of you OctoPrint (or OctoPi) host. Used to get 3D printer stats.
|
||||
# octoprint_ip_address - IP Address of you OctoPrint (or OctoPi) host. Used to get 3D printer stats.
|
||||
# bambu_ip_address - IP Address of your Bambu Lab printer on the local network
|
||||
# bambu_access_code - Access code displayed on the printer's screen (Settings > WLAN)
|
||||
# bambu_serial_number - Serial number of your Bambu Lab printer
|
||||
# bambu_camera_enable - Set to true to capture a camera snapshot with $bambustat via ffmpeg RTSPS stream.
|
||||
# Requires ffmpeg to be installed on the bot host.
|
||||
# statping_url - Server hostname of the StatPing instance we will be querying w/ the REST API
|
||||
# statping_api_key - API Key for Statping to query for service status
|
||||
#
|
||||
discord_auth_token = <Discord Auth Token>
|
||||
bot_command_prefix = <!/$/#/etc>
|
||||
hass_url = <URL to HomeAssistant>
|
||||
hass_token = <HomeAssistant Long Lived Access Token>
|
||||
hass_light = <Entity_ID of the light @ your printer>
|
||||
octoprint_ip_address = <OctoPrint IP Address>
|
||||
|
||||
bambu_ip_address = <Bambu Lab Printer IP Address>
|
||||
bambu_access_code = <Bambu Lab Access Code>
|
||||
bambu_serial_number = <Bambu Lab Serial Number>
|
||||
bambu_camera_enable = false
|
||||
statping_enable = <trues/false>
|
||||
statping_url = <Statping URL>
|
||||
statping_api_key = <Statping API Key>
|
||||
mmr_checker_enable = <true/false>
|
||||
mmr_checker_interval = <number of seconds between checks>
|
||||
mmr_api_url = <MMR API URL>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
+471
-131
@@ -1,40 +1,63 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext import commands, tasks
|
||||
from datetime import datetime
|
||||
import urllib.request
|
||||
import random
|
||||
import os
|
||||
import sys
|
||||
import octoapi
|
||||
import time
|
||||
from datetime import datetime
|
||||
import configparser
|
||||
import json
|
||||
from discord import FFmpegPCMAudio
|
||||
from discord.utils import get
|
||||
from discord.voice_client import VoiceClient
|
||||
import requests
|
||||
import asyncio
|
||||
from bambu_connect import BambuClient
|
||||
|
||||
bot = commands.Bot(command_prefix='$', description='A derpy derp of a bot.')
|
||||
# Monkey-patch every dataclass in bambu-connect's models module to tolerate
|
||||
# unknown fields. The Bambu printer firmware sends extra keys that the
|
||||
# bambu-connect 0.3.1 models don't define (e.g. Upload.file_size,
|
||||
# Online.ext), causing TypeErrors that crash the MQTT thread.
|
||||
# Filtering kwargs to known fields before calling the original __init__
|
||||
# fixes this for all nested classes at once.
|
||||
import dataclasses as _dc
|
||||
import inspect as _inspect
|
||||
from bambu_connect.utils import models as _bambu_models
|
||||
|
||||
def _make_permissive(cls):
|
||||
known = {f.name for f in _dc.fields(cls)}
|
||||
orig = cls.__init__
|
||||
def __init__(self, **kwargs):
|
||||
orig(self, **{k: v for k, v in kwargs.items() if k in known})
|
||||
cls.__init__ = __init__
|
||||
|
||||
for _name, _cls in _inspect.getmembers(_bambu_models, _inspect.isclass):
|
||||
if _dc.is_dataclass(_cls):
|
||||
_make_permissive(_cls)
|
||||
|
||||
#Record the time the bot started
|
||||
start_time = time.time()
|
||||
start_time = time.time()
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
configFilePath = r'bot_config.conf'
|
||||
|
||||
# Last error message variable used by $last_error command to display in discord
|
||||
global last_error
|
||||
last_error = "No errors been recorded."
|
||||
|
||||
try:
|
||||
config.read(configFilePath)
|
||||
except:
|
||||
print("Error loading config! Please check config file 'bot-config.conf'")
|
||||
|
||||
# Setup HomeAssistant URL and Headers
|
||||
# Last error message variable used by $last_error command to display in discord
|
||||
last_error_msg = "No errors have been recorded."
|
||||
|
||||
#
|
||||
# Assign bot config from setting in config file
|
||||
#
|
||||
|
||||
# Assign Bot core settings
|
||||
DISCORD_AUTH_TOKEN = config.get('bot-config', 'discord_auth_token')
|
||||
BOT_COMMAND_PREFIX = config.get('bot-config', 'bot_command_prefix')
|
||||
|
||||
# 3D Printer Setup - HomeAssistant URL and Headers & OctoPrint
|
||||
HASS_TOKEN = config.get('bot-config', 'hass_token')
|
||||
HASS_URL = config.get('bot-config', 'hass_url')
|
||||
HASS_LIGHT = config.get('bot-config', 'hass_light')
|
||||
@@ -43,28 +66,164 @@ hassHEADERS = {
|
||||
'Authorization': f"Bearer {HASS_TOKEN}",
|
||||
'content-type': 'application/json',
|
||||
}
|
||||
|
||||
OCTOPRINT_IP_ADDRESS = config.get('bot-config', 'octoprint_ip_address')
|
||||
|
||||
DISCORD_AUTH_TOKEN = config.get('bot-config', 'discord_auth_token')
|
||||
# Bambu Lab Printer config (optional — feature is skipped if not set)
|
||||
BAMBU_IP_ADDRESS = config.get('bot-config', 'bambu_ip_address', fallback=None)
|
||||
BAMBU_ACCESS_CODE = config.get('bot-config', 'bambu_access_code', fallback=None)
|
||||
BAMBU_SERIAL_NUMBER = config.get('bot-config', 'bambu_serial_number', fallback=None)
|
||||
BAMBU_CAMERA_ENABLE = config.get('bot-config', 'bambu_camera_enable', fallback='false').lower() == 'true'
|
||||
|
||||
# Module-level cache for Bambu printer status, updated by the MQTT watch callback
|
||||
bambu_status = None
|
||||
bambu_client = None
|
||||
_bot_loop = None # set in on_ready; used by the MQTT thread
|
||||
_last_bambu_activity_update = 0.0
|
||||
_bambu_was_printing = False
|
||||
|
||||
|
||||
def _on_bambu_update(status):
|
||||
"""Callback invoked by bambu-connect's MQTT thread with the latest PrinterStatus."""
|
||||
global bambu_status, _last_bambu_activity_update, _bambu_was_printing
|
||||
bambu_status = status
|
||||
|
||||
if _bot_loop is None:
|
||||
return
|
||||
|
||||
now = time.time()
|
||||
if status.gcode_state == "RUNNING":
|
||||
_bambu_was_printing = True
|
||||
# Throttle Discord presence updates to once every 30 seconds
|
||||
if (now - _last_bambu_activity_update) >= 30:
|
||||
_last_bambu_activity_update = now
|
||||
asyncio.run_coroutine_threadsafe(_update_bambu_activity(status), _bot_loop)
|
||||
elif _bambu_was_printing:
|
||||
# State just left RUNNING (paused, finished, failed) — reset presence
|
||||
_bambu_was_printing = False
|
||||
asyncio.run_coroutine_threadsafe(_reset_bot_activity(), _bot_loop)
|
||||
|
||||
|
||||
# Configure bot
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.voice_states = True
|
||||
bot = commands.Bot(command_prefix=BOT_COMMAND_PREFIX, description='A derpy derp of a bot.', intents=intents)
|
||||
bot.remove_command('help')
|
||||
|
||||
# Load opus once at startup rather than on every sound command
|
||||
discord.opus.load_opus("libopus.so")
|
||||
|
||||
# Printing configuration details to console
|
||||
print(f"HomeAssistant URL: {hassURL}")
|
||||
print(f"HomeAssistant URL HEADERS: {hassHEADERS}")
|
||||
print(f"HomeAssistant Light: {HASS_LIGHT}")
|
||||
print(f"OctoPrint IP Address: {OCTOPRINT_IP_ADDRESS}")
|
||||
print("=== 3D Printer Status config ===")
|
||||
print(f" HomeAssistant URL: {hassURL}")
|
||||
print(f" HomeAssistant URL HEADERS: {hassHEADERS}")
|
||||
print(f" HomeAssistant Light: {HASS_LIGHT}")
|
||||
print(f" OctoPrint IP Address: {OCTOPRINT_IP_ADDRESS}")
|
||||
|
||||
async def _update_bambu_activity(status):
|
||||
remaining_mins = status.mc_remaining_time or 0
|
||||
remaining_hours = remaining_mins // 60
|
||||
remaining_mins_part = remaining_mins % 60
|
||||
activity = discord.Activity(
|
||||
name=f"Bambu @ {status.mc_percent}%, {remaining_hours}h{remaining_mins_part:02d}m to go",
|
||||
type=discord.ActivityType.watching,
|
||||
)
|
||||
await bot.change_presence(activity=activity)
|
||||
|
||||
|
||||
async def _capture_bambu_frame(filename):
|
||||
"""Capture a single JPEG frame from the Bambu RTSPS stream via ffmpeg.
|
||||
Returns True on success, raises on timeout or ffmpeg error."""
|
||||
rtsp_url = f"rtsps://bblp:{BAMBU_ACCESS_CODE}@{BAMBU_IP_ADDRESS}/streaming/live/1"
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg", "-y",
|
||||
"-rtsp_transport", "tcp",
|
||||
"-i", rtsp_url,
|
||||
"-vframes", "1",
|
||||
"-q:v", "2",
|
||||
filename,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
|
||||
if proc.returncode != 0:
|
||||
print(f" ! ffmpeg stderr: {stderr.decode(errors='replace').strip()}")
|
||||
return proc.returncode == 0
|
||||
|
||||
|
||||
async def _reset_bot_activity():
|
||||
activity = discord.Activity(
|
||||
name=f"your {BOT_COMMAND_PREFIX}commands",
|
||||
type=discord.ActivityType.listening,
|
||||
)
|
||||
await bot.change_presence(activity=activity)
|
||||
|
||||
|
||||
# Background task: poll OctoPrint every 2 minutes and update bot status if printing
|
||||
@tasks.loop(seconds=120)
|
||||
async def check_printer_status():
|
||||
try:
|
||||
print(">> Checking 3D Printer Status")
|
||||
# Run the synchronous octoapi call in a thread so it doesn't block
|
||||
# the event loop and starve Discord's heartbeat on slow/unreachable hosts
|
||||
loop = asyncio.get_running_loop()
|
||||
print_completion = round(await loop.run_in_executor(None, octoapi.get_completion), 2)
|
||||
except Exception:
|
||||
print_completion = "Unknown"
|
||||
print(">>>> Nothing Printing.")
|
||||
if print_completion != "Unknown" and print_completion < 100:
|
||||
print(">>>> Something is printing, calling UpdateStatus")
|
||||
await updateStatus()
|
||||
else:
|
||||
# Will hit this if the last print job completed and is sitting at 100%
|
||||
print(">>>> Nothing Printing.")
|
||||
|
||||
|
||||
# On Ready
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print("\n:::: Config Loaded ::::\n")
|
||||
print('Logged in as')
|
||||
print(bot.user.name)
|
||||
print(bot.user.id)
|
||||
print('------')
|
||||
print("Setting activity to 'Listenting to your commands")
|
||||
activity = discord.Activity(name="your commands",type=discord.ActivityType.listening)
|
||||
print(f" User: {bot.user.name}")
|
||||
print(f" ID: {bot.user.id}")
|
||||
print(f"\n\n")
|
||||
print(f"Setting activity to 'Listenting to your {BOT_COMMAND_PREFIX}commands'")
|
||||
activity = discord.Activity(name=f"your {BOT_COMMAND_PREFIX}commands",type=discord.ActivityType.listening)
|
||||
await bot.change_presence(activity=activity)
|
||||
|
||||
print(">> Startup complete.")
|
||||
|
||||
# Clear any stale voice states left over from a previous session.
|
||||
# Discord holds onto the bot's voice state across restarts; if not cleared,
|
||||
# the first voice connect attempt gets a 4006 (session no longer valid).
|
||||
print(">> Clearing stale voice states across all guilds")
|
||||
for guild in bot.guilds:
|
||||
try:
|
||||
await guild.change_voice_state(channel=None)
|
||||
except Exception as e:
|
||||
print(f" ! Could not clear voice state for {guild.name}: {e}")
|
||||
|
||||
# Start the printer polling task (guard against double-start on reconnects)
|
||||
if not check_printer_status.is_running():
|
||||
check_printer_status.start()
|
||||
|
||||
# Store the running event loop so the Bambu MQTT thread can schedule coroutines
|
||||
global _bot_loop
|
||||
_bot_loop = asyncio.get_running_loop()
|
||||
|
||||
# Start the Bambu watch client if config is present
|
||||
global bambu_client
|
||||
if BAMBU_IP_ADDRESS and BAMBU_ACCESS_CODE and BAMBU_SERIAL_NUMBER:
|
||||
if bambu_client is None:
|
||||
print(">> Connecting to Bambu printer MQTT watch client")
|
||||
bambu_client = BambuClient(BAMBU_IP_ADDRESS, BAMBU_ACCESS_CODE, BAMBU_SERIAL_NUMBER)
|
||||
bambu_client.start_watch_client(_on_bambu_update)
|
||||
print(">> Bambu watch client started")
|
||||
else:
|
||||
print(">> Bambu printer config not found, skipping Bambu watch client")
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def greetings(ctx):
|
||||
await ctx.send(":smiley: :wave: Hello there!")
|
||||
@@ -72,41 +231,26 @@ async def greetings(ctx):
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx, error):
|
||||
global last_error_msg
|
||||
last_error_msg = error
|
||||
if isinstance(error, commands.CommandOnCooldown):
|
||||
seconds = round(error.retry_after,1)
|
||||
print(f" - Error, user tried to use command while in cooldown (wait is: {seconds}")
|
||||
print(f" - Error, user tried to use command while in cooldown (wait is: {seconds})")
|
||||
await ctx.send(f"This command is ratelimited per user, please try again in {seconds}s")
|
||||
if isinstance(error, commands.CommandInvokeError):
|
||||
elif isinstance(error, commands.CommandInvokeError):
|
||||
print(" - ERROR: encountered 'CommandInvokeError'")
|
||||
print(f" - Dumping error: {error}")
|
||||
last_error = error
|
||||
await ctx.send("Error, unable to complete your request.")
|
||||
else:
|
||||
print('Oopsie, I found an error...')
|
||||
print(f"Error: {error}")
|
||||
last_error = error
|
||||
|
||||
#
|
||||
# Debug commands
|
||||
#
|
||||
@bot.command()
|
||||
async def last_error(ctx):
|
||||
await ctx.send(f"Last error recorded: {last_error}")
|
||||
|
||||
@bot.command()
|
||||
async def uptime(ctx):
|
||||
# Displays how long the bot has been online
|
||||
second = time.time() - start_time
|
||||
minute, second = divmod(second, 60)
|
||||
hour, minute = divmod(minute, 60)
|
||||
day, hour = divmod(hour, 24)
|
||||
week, day = divmod(day, 7)
|
||||
week = int(week)
|
||||
day = int(day)
|
||||
hour = int(hour)
|
||||
minute = int(minute)
|
||||
second = int(second)
|
||||
await ctx.send(f"Uptime [WW:DD:HH:MM:SS]: {week:02d}:{day:02d}:{hour:02d}:{minute:02d}:{second:02d}")
|
||||
await ctx.send(f"Last error recorded: {last_error_msg}")
|
||||
|
||||
#
|
||||
# Giphy Commands
|
||||
@@ -137,14 +281,13 @@ async def smudge(ctx):
|
||||
#
|
||||
# Voice Commands
|
||||
#
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=30.0, type=commands.BucketType.user)
|
||||
async def bitch(ctx, member : discord.Member="NONE"):
|
||||
datestring = datetime.now()
|
||||
datestring = datestring.strftime("%m/%d/%Y-%H:%M:%S")
|
||||
if ctx.message.channel.is_nsfw():
|
||||
await play_sound(ctx, member, "./sounds/monkey_bitch.mp3", "$bitch", 5)
|
||||
await play_sound(ctx, member, "./sounds/monkey_bitch.mp3", "$bitch")
|
||||
else:
|
||||
print(f"[{datestring}]: {ctx.message.author.display_name} called $bitch, but is not in a NSFW channel")
|
||||
await ctx.send("This command is too explicit for you!")
|
||||
@@ -152,7 +295,7 @@ async def bitch(ctx, member : discord.Member="NONE"):
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def cowbell(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/More_cowbell.mp3", "$cowbell", 5)
|
||||
await play_sound(ctx, member, "./sounds/More_cowbell.mp3", "$cowbell")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
@@ -160,7 +303,7 @@ async def boom(ctx, member : discord.Member="NONE"):
|
||||
datestring = datetime.now()
|
||||
datestring = datestring.strftime("%m/%d/%Y-%H:%M:%S")
|
||||
if ctx.message.channel.is_nsfw():
|
||||
await play_sound(ctx, member, "./sounds/BoomBitch.mp3", "$boom", 5)
|
||||
await play_sound(ctx, member, "./sounds/BoomBitch.mp3", "$boom")
|
||||
else:
|
||||
print(f"[{datestring}]: {ctx.message.author.display_name} called $boom, but is not in a NSFW channel")
|
||||
await ctx.send("This command is too explicit for you!")
|
||||
@@ -168,37 +311,71 @@ async def boom(ctx, member : discord.Member="NONE"):
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def oops(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/Oops.mp3", "$oops", 5)
|
||||
await play_sound(ctx, member, "./sounds/Oops.mp3", "$oops")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def trololo(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/Trololo.mp3", "$trololo", 5)
|
||||
await play_sound(ctx, member, "./sounds/Trololo.mp3", "$trololo")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def leeroy(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/leeroy.mp3", "$leeroy", 5)
|
||||
await play_sound(ctx, member, "./sounds/leeroy.mp3", "$leeroy")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def promoted(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/Promoted.mp3", "$promoted", 5)
|
||||
await play_sound(ctx, member, "./sounds/Promoted.mp3", "$promoted")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def wow(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/wow.mp3", "$wow", 5)
|
||||
await play_sound(ctx, member, "./sounds/wow.mp3", "$wow")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def aww(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/Awww_Bitch.mp3", "$aww", 5)
|
||||
await play_sound(ctx, member, "./sounds/Awww_Bitch.mp3", "$aww")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def eia(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/awesome.mp3", "$eia", 11)
|
||||
await play_sound(ctx, member, "./sounds/awesome.mp3", "$eia")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def yaw(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/yaw.mp3", "$yaw")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def heavy(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/heavy.mp3", "$heavy")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def iran(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/iran.mp3", "$iran")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def big(ctx, member : discord.Member="NONE"):
|
||||
await play_sound(ctx, member, "./sounds/big.mp3", "$big")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def breee(ctx, member : discord.Member="NONE"):
|
||||
await ctx.send(f"\m/ ( ゚Д゚)<BREEEEE!! \m/")
|
||||
await play_sound(ctx, member, "./sounds/wbbwb_metal.mp3", "$breee")
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
async def nice(ctx, member : discord.Member="NONE"):
|
||||
await ctx.send(f"\m/ ( ゚Д゚)<N!CE! \m/")
|
||||
await play_sound(ctx, member, "./sounds/wbtbwb-nice.mp3", "$nice")
|
||||
|
||||
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=10.0, type=commands.BucketType.user)
|
||||
@@ -212,9 +389,7 @@ async def rs(ctx, member : discord.Member="NONE"):
|
||||
print(f"[{datestring}]: {ctx.message.author.display_name} called $rs, but is not in a NSFW channel")
|
||||
await ctx.send("This command is too explicit for you!")
|
||||
|
||||
async def play_sound(ctx, member : discord.Member, soundFile, command, playtime):
|
||||
|
||||
discord.opus.load_opus("libopus.so")
|
||||
async def play_sound(ctx, member : discord.Member, soundFile, command):
|
||||
|
||||
datestring = datetime.now()
|
||||
datestring = datestring.strftime("%m/%d/%Y-%H:%M:%S")
|
||||
@@ -226,7 +401,7 @@ async def play_sound(ctx, member : discord.Member, soundFile, command, playtime)
|
||||
if member != "NONE":
|
||||
try:
|
||||
channel = member.voice.channel
|
||||
except:
|
||||
except AttributeError:
|
||||
await ctx.send(f"{member.display_name} is not in a voice channel!")
|
||||
print(f" ! {member.display_name} is not in a voice channel")
|
||||
return
|
||||
@@ -236,7 +411,7 @@ async def play_sound(ctx, member : discord.Member, soundFile, command, playtime)
|
||||
if member == "NONE":
|
||||
try:
|
||||
channel = ctx.message.author.voice.channel
|
||||
except:
|
||||
except AttributeError:
|
||||
await ctx.send("You are not connected to a voice channel.")
|
||||
print("User is not connected to a voice channel")
|
||||
return
|
||||
@@ -254,14 +429,17 @@ async def play_sound(ctx, member : discord.Member, soundFile, command, playtime)
|
||||
else:
|
||||
print(f" - Joining Channel '{channel}''")
|
||||
await ctx.send(f"Joining '{channel}' for just a moment")
|
||||
voice = await channel.connect()
|
||||
# reconnect=False disables discord.py's internal retry loop so it doesn't
|
||||
# hammer the same invalid session repeatedly (which was causing "attempt 4").
|
||||
voice = await channel.connect(reconnect=False)
|
||||
|
||||
print(" - Creating Player")
|
||||
audio_source = discord.FFmpegPCMAudio(soundFile)
|
||||
print(f" - Playing {soundFile}")
|
||||
time.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
voice.play(audio_source, after=None)
|
||||
time.sleep(playtime)
|
||||
while voice.is_playing():
|
||||
await asyncio.sleep(1)
|
||||
print(f" - Disconnecting from '{channel}'")
|
||||
await voice.disconnect()
|
||||
###
|
||||
@@ -277,25 +455,22 @@ async def printstat(ctx):
|
||||
|
||||
print(f"[{datestring}]: {ctx.message.author.display_name} called $printstat")
|
||||
|
||||
#print("========== Command Called at %s ==========" % (time.strftime("%Y%m%d-%H%M%S")))
|
||||
# We haven't yet changed the state of the light, setting this to False
|
||||
turned_on_light = False
|
||||
|
||||
# No Data from Printer?
|
||||
operational = True
|
||||
|
||||
# Try to get the status of the 3D printer light
|
||||
print(" - Getting Printer Light Status")
|
||||
work_lights = requests.get(hassURL, headers=hassHEADERS)
|
||||
print(f" JSON Response from HomeAssistant: {work_lights.text}")
|
||||
loop = asyncio.get_running_loop()
|
||||
work_lights_resp = await loop.run_in_executor(None, lambda: requests.get(hassURL, headers=hassHEADERS))
|
||||
print(f" JSON Response from HomeAssistant: {work_lights_resp.text}")
|
||||
|
||||
work_lights = json.loads(work_lights.text)
|
||||
work_lights = json.loads(work_lights_resp.text)
|
||||
|
||||
# Making sure that the API request succeeded. If it has, there will be a state attribute added to
|
||||
# work_lights.
|
||||
# work_lights.
|
||||
try:
|
||||
work_lights["state"]
|
||||
except:
|
||||
except KeyError:
|
||||
print(" ! Unable to get light status from Homeassistant, notifying chat channel")
|
||||
await ctx.send("Hmm, I can't tell if the light is on... oh well [Hass API error]")
|
||||
|
||||
@@ -317,19 +492,17 @@ async def printstat(ctx):
|
||||
await ctx.send(f"{random.choice(lightMessages)}")
|
||||
|
||||
try:
|
||||
# REST API call to home assistant to turn the light off.
|
||||
# REST API call to home assistant to turn the light on.
|
||||
hassAPIURL = HASS_URL + "/api/services/switch/turn_on"
|
||||
payload = f'{{"entity_id": "{HASS_LIGHT}"}}'
|
||||
#print(f" - Payload String: {payload}")
|
||||
response = requests.post(hassAPIURL, headers=hassHEADERS, data=payload)
|
||||
#print(f" - POST Response: {response.text}")
|
||||
await loop.run_in_executor(None, lambda: requests.post(hassAPIURL, headers=hassHEADERS, data=payload))
|
||||
|
||||
except Exception as e:
|
||||
print(f" ! Unable to turn on light; ERROR: {e}")
|
||||
await ctx.send("Sorry, I was unable to turn on the light.")
|
||||
pass
|
||||
|
||||
time.sleep(3)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Randomly generate a filename to save the image to
|
||||
file_name = random.randrange(1,10000)
|
||||
@@ -337,7 +510,8 @@ async def printstat(ctx):
|
||||
|
||||
# Get an image from OctoPrint and save it
|
||||
print(" - Getting image from Webcam")
|
||||
urllib.request.urlretrieve("http://%s:8080/?action=snapshot" % (OCTOPRINT_IP_ADDRESS), full_file_name)
|
||||
await loop.run_in_executor(None, lambda: urllib.request.urlretrieve(
|
||||
"http://%s:8080/?action=snapshot" % (OCTOPRINT_IP_ADDRESS), full_file_name))
|
||||
|
||||
# If we turned on the light, let's be nice and turn it back off
|
||||
if turned_on_light:
|
||||
@@ -346,8 +520,7 @@ async def printstat(ctx):
|
||||
# REST API call to home assistant to turn the light off.
|
||||
hassAPIURL = HASS_URL + "/api/services/switch/turn_off"
|
||||
payload = f'{{"entity_id": "{HASS_LIGHT}"}}'
|
||||
response = requests.post(hassAPIURL, headers=hassHEADERS, data=payload)
|
||||
#print(f" - POST Response: {response.text}")
|
||||
await loop.run_in_executor(None, lambda: requests.post(hassAPIURL, headers=hassHEADERS, data=payload))
|
||||
|
||||
except Exception as e:
|
||||
print(f" ! Unable to turn on light; ERROR: {e}")
|
||||
@@ -366,75 +539,78 @@ async def printstat(ctx):
|
||||
print(" - Connecting to OctoPrint API")
|
||||
# Testing OctoPrint API Status. If printer is off, I won't receive JSON data.
|
||||
try:
|
||||
operational = json.loads(octoapi.get_printer_dict())
|
||||
except:
|
||||
operational = await loop.run_in_executor(None, octoapi.get_printer_dict)
|
||||
except Exception:
|
||||
# Printer is Offline
|
||||
operational = False
|
||||
|
||||
if not operational:
|
||||
|
||||
if operational:
|
||||
print(" - 3D Printer is on and printing, getting details")
|
||||
|
||||
# Get the name of the file currently being printed
|
||||
try:
|
||||
print(" - Getting Filename")
|
||||
print_filename = octoapi.get_printFileName()
|
||||
except:
|
||||
print_filename = await loop.run_in_executor(None, octoapi.get_printFileName)
|
||||
except Exception:
|
||||
print_filename = ""
|
||||
|
||||
try:
|
||||
print(" - Getting % Completion")
|
||||
# Try to get the % of Completion
|
||||
print_completion = round(octoapi.get_completion(), 2)
|
||||
except:
|
||||
print_completion = round(await loop.run_in_executor(None, octoapi.get_completion), 2)
|
||||
except Exception:
|
||||
print_completion = "Unknown"
|
||||
|
||||
# Get the current print time in seconds
|
||||
try:
|
||||
print(" - Getting Seconds Elapsed")
|
||||
print_seconds = octoapi.get_printTime()
|
||||
except:
|
||||
print_seconds = await loop.run_in_executor(None, octoapi.get_printTime)
|
||||
except Exception:
|
||||
print("Unable to get Seconds data from OctoPrint")
|
||||
print_seconds = 0
|
||||
|
||||
# Get the time remaining for current print job in seconds
|
||||
try:
|
||||
print(" - Getting Seconds Left")
|
||||
print_secondsleft = octoapi.get_printTimeLeft()
|
||||
except:
|
||||
print_secondsleft = await loop.run_in_executor(None, octoapi.get_printTimeLeft)
|
||||
except Exception:
|
||||
print(" ! Unable to get Seconds Left from OctoPrint")
|
||||
print_secondsleft = 0
|
||||
|
||||
try:
|
||||
print(" - Converting Seconds to Hours")
|
||||
# Try to convert seconds to hours
|
||||
print_hours = int(((print_seconds / 60) / 60))
|
||||
except:
|
||||
except (TypeError, ValueError):
|
||||
print_hours = "Unknown"
|
||||
|
||||
try:
|
||||
print(" - Converting Seconds left to Hours left")
|
||||
# Try to convert seconds left to hours left
|
||||
print_hoursleft = int(((print_secondsleft / 60) / 60))
|
||||
except:
|
||||
except (TypeError, ValueError):
|
||||
print_hoursleft = "Unknown"
|
||||
|
||||
|
||||
try:
|
||||
print(" - Calculating Minutes Elapsed")
|
||||
# Using the total number of minutes minus the total number of whole hours to get the minutes remaining
|
||||
print_min = int(((print_seconds / 60) - (print_hours * 60)))
|
||||
except:
|
||||
except (TypeError, ValueError):
|
||||
print_min = "Unknown"
|
||||
|
||||
|
||||
try:
|
||||
print(" - Calculating Minutes Left")
|
||||
# Same as above but for the minutes left in print job
|
||||
print_minleft = int(((print_secondsleft / 60) - (print_hoursleft * 60)))
|
||||
except:
|
||||
except (TypeError, ValueError):
|
||||
print_minleft = "Unknown"
|
||||
|
||||
time_elapsed = "%s Hours %s Minutes" % (print_hours, print_min)
|
||||
time_remaining = "%s Hours %s Minutes" % (print_hoursleft, print_minleft)
|
||||
|
||||
printer_bed = "%s°C" % (octoapi.get_printer_dict()["temperature"]["bed"]["actual"])
|
||||
printer_hotend = "%s°C" % (octoapi.get_printer_dict()["temperature"]["tool0"]["actual"])
|
||||
# operational dict already fetched above — use it directly instead of calling again
|
||||
printer_bed = "%s°C" % (operational["temperature"]["bed"]["actual"])
|
||||
printer_hotend = "%s°C" % (operational["temperature"]["tool0"]["actual"])
|
||||
|
||||
embed = discord.Embed(title="R0b3's 3D Printer Status", description="Current Job: " + print_filename, color=0xF5A623)
|
||||
|
||||
@@ -443,11 +619,14 @@ async def printstat(ctx):
|
||||
embed.add_field(name="Time Remaining: ", value=time_remaining)
|
||||
embed.add_field(name="Bed Temp.: ", value=printer_bed)
|
||||
embed.add_field(name="Hotend Temp.: ", value=printer_hotend)
|
||||
embed.add_field(name="Livestream URL: ",value="https://rickelobe.com/printer/?action=stream")
|
||||
print(" - Sending details to chat channel")
|
||||
await ctx.send(embed=embed)
|
||||
activity = discord.Activity(name=f"3D Print @ {str(print_completion)}%",type=discord.ActivityType.watching)
|
||||
await bot.change_presence(activity=activity)
|
||||
await updateStatus()
|
||||
|
||||
## No longer need to update the bot status here since there's a check happening every 2 mins in on_ready
|
||||
#activity = discord.Activity(name=f"3D Print @ {str(print_completion)}%",type=discord.ActivityType.watching)
|
||||
#await bot.change_presence(activity=activity)
|
||||
#await updateStatus() # No longer need to call this since it the bot now checks for a print job every 2 mins.
|
||||
|
||||
else:
|
||||
print(" - 3D Printer is not on or is not printing")
|
||||
@@ -458,27 +637,55 @@ async def printstat(ctx):
|
||||
async def updateStatus():
|
||||
|
||||
# Initialize variables
|
||||
# Unknowns is present to handle issues like print paused or cancelled
|
||||
print_completion = 0
|
||||
unknowns = 0
|
||||
loop = asyncio.get_running_loop()
|
||||
#While the print status is less than 100 % complete, keep updating
|
||||
while print_completion < 100:
|
||||
try:
|
||||
print(" - Getting % Completion")
|
||||
# Try to get the % of Completion
|
||||
print_completion = round(octoapi.get_completion(), 2)
|
||||
except:
|
||||
print_completion = round(await loop.run_in_executor(None, octoapi.get_completion), 1)
|
||||
except Exception:
|
||||
print_completion = 999
|
||||
unknowns += 1
|
||||
|
||||
|
||||
try:
|
||||
print_secondsleft = await loop.run_in_executor(None, octoapi.get_printTimeLeft)
|
||||
except Exception:
|
||||
print(" ! Unable to get Seconds Left from OctoPrint")
|
||||
print_secondsleft = 0
|
||||
|
||||
try:
|
||||
# Try to convert seconds left to hours left
|
||||
print_hoursleft = int(((print_secondsleft / 60) / 60))
|
||||
except (TypeError, ValueError):
|
||||
print_hoursleft = "Unknown"
|
||||
|
||||
try:
|
||||
# Same as above but for the minutes left in print job
|
||||
print_minleft = int(((print_secondsleft / 60) - (print_hoursleft * 60)))
|
||||
except (TypeError, ValueError):
|
||||
print_minleft = "Unknown"
|
||||
|
||||
if print_minleft == "Unknown" or print_hoursleft == "Unknown":
|
||||
time_left = "Unknown"
|
||||
else:
|
||||
time_left = f"{print_hoursleft:02d}:{print_minleft:02d}"
|
||||
|
||||
#Set the activity to the new percent complete value
|
||||
if print_completion != 999:
|
||||
activity = discord.Activity(name=f"3D Print @ {str(print_completion)}%",type=discord.ActivityType.watching)
|
||||
unknowns = 0
|
||||
print(f" - Print job is at {print_completion}%, with {time_left} to go")
|
||||
activity = discord.Activity(name=f"3D Print @ {str(print_completion)}%, {time_left} to go",type=discord.ActivityType.watching)
|
||||
else:
|
||||
unknowns += 1
|
||||
print(f" - Error getting print status ({unknowns}/5 Errors)")
|
||||
activity = discord.Activity(name=f"3D Print @ ERROR",type=discord.ActivityType.watching)
|
||||
|
||||
await bot.change_presence(activity=activity)
|
||||
|
||||
if (unknowns >= 5):
|
||||
print(" - Reached 5 consecutive errors, breaking from loop")
|
||||
break
|
||||
await asyncio.sleep(30)
|
||||
|
||||
@@ -494,9 +701,80 @@ async def updateStatus():
|
||||
|
||||
print(" - Done watching 3D Printer status")
|
||||
await asyncio.sleep(3)
|
||||
activity = discord.Activity(name="your commands",type=discord.ActivityType.listening)
|
||||
activity = discord.Activity(name=f"your {BOT_COMMAND_PREFIX}commands",type=discord.ActivityType.listening)
|
||||
await bot.change_presence(activity=activity)
|
||||
|
||||
#
|
||||
# Bambu Lab Printer Commands
|
||||
#
|
||||
@bot.command()
|
||||
async def bambustat(ctx):
|
||||
|
||||
datestring = datetime.now().strftime("%m/%d/%Y-%H:%M:%S")
|
||||
print(f"[{datestring}]: {ctx.message.author.display_name} called $bambustat")
|
||||
|
||||
if bambu_status is None:
|
||||
await ctx.send("Bambu printer status is not available yet. The printer may be offline or still connecting.")
|
||||
return
|
||||
|
||||
status = bambu_status
|
||||
|
||||
gcode_state = status.gcode_state or "Unknown"
|
||||
gcode_file = status.gcode_file or "Unknown"
|
||||
mc_percent = status.mc_percent if status.mc_percent is not None else "Unknown"
|
||||
|
||||
# mc_remaining_time is in minutes
|
||||
remaining_mins = status.mc_remaining_time or 0
|
||||
remaining_hours = remaining_mins // 60
|
||||
remaining_mins_part = remaining_mins % 60
|
||||
time_remaining = f"{remaining_hours}h {remaining_mins_part:02d}m"
|
||||
|
||||
nozzle_temp = f"{status.nozzle_temper:.1f}°C" if status.nozzle_temper is not None else "Unknown"
|
||||
bed_temp = f"{status.bed_temper:.1f}°C" if status.bed_temper is not None else "Unknown"
|
||||
chamber_temp = f"{status.chamber_temper:.1f}°C" if status.chamber_temper is not None else "Unknown"
|
||||
|
||||
layer_info = "Unknown"
|
||||
if status.layer_num is not None and status.total_layer_num is not None:
|
||||
layer_info = f"{status.layer_num} / {status.total_layer_num}"
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Bambu Lab Printer Status",
|
||||
description=f"File: {gcode_file}",
|
||||
color=0x00B4D8,
|
||||
)
|
||||
embed.add_field(name="State:", value=gcode_state, inline=False)
|
||||
embed.add_field(name="Percent Complete:", value=f"{mc_percent}%")
|
||||
embed.add_field(name="Time Remaining:", value=time_remaining)
|
||||
embed.add_field(name="Layer:", value=layer_info)
|
||||
embed.add_field(name="Nozzle Temp:", value=nozzle_temp)
|
||||
embed.add_field(name="Bed Temp:", value=bed_temp)
|
||||
embed.add_field(name="Chamber Temp:", value=chamber_temp)
|
||||
|
||||
# Try to grab a snapshot via ffmpeg RTSPS (only if enabled in config)
|
||||
image_filename = None
|
||||
if BAMBU_CAMERA_ENABLE and BAMBU_IP_ADDRESS and BAMBU_ACCESS_CODE:
|
||||
image_filename = f"bambu_{random.randrange(1, 10000)}.jpg"
|
||||
try:
|
||||
print(" - Capturing Bambu camera frame via RTSPS")
|
||||
success = await _capture_bambu_frame(image_filename)
|
||||
if not success:
|
||||
print(" ! ffmpeg exited with non-zero code, skipping image")
|
||||
image_filename = None
|
||||
except asyncio.TimeoutError:
|
||||
print(" ! ffmpeg timed out capturing Bambu camera frame")
|
||||
image_filename = None
|
||||
except Exception as e:
|
||||
print(f" ! Unable to capture Bambu camera frame: {e}")
|
||||
image_filename = None
|
||||
|
||||
print(" - Sending Bambu details to chat channel")
|
||||
if image_filename:
|
||||
file = discord.File(image_filename, filename=image_filename)
|
||||
await ctx.send(file=file, embed=embed)
|
||||
os.remove(image_filename)
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def info(ctx):
|
||||
@@ -511,34 +789,96 @@ async def info(ctx):
|
||||
# give users a link to invite thsi bot to their server
|
||||
embed.add_field(name="Invite", value="[Invite link](https://discordapp.com/api/oauth2/authorize?client_id=416312716677087233&permissions=51264&scope=bot)")
|
||||
|
||||
# Bot Uptime
|
||||
second = time.time() - start_time
|
||||
minute, second = divmod(second, 60)
|
||||
hour, minute = divmod(minute, 60)
|
||||
day, hour = divmod(hour, 24)
|
||||
week, day = divmod(day, 7)
|
||||
week = int(week)
|
||||
day = int(day)
|
||||
hour = int(hour)
|
||||
minute = int(minute)
|
||||
second = int(second)
|
||||
embed.add_field(name="Uptime [WW:DD:HH:MM:SS]:", value=f"{week:02d}:{day:02d}:{hour:02d}:{minute:02d}:{second:02d}", inline=False)
|
||||
|
||||
print("Getting git describe data to display as version in info embed")
|
||||
bot_version = await run('git describe --tags')
|
||||
embed.add_field(name="Version:", value=f"{bot_version}", inline=False)
|
||||
embed.add_field(name="discord.py Version:", value=f"{discord.__version__}", inline=False)
|
||||
|
||||
# Gitlab link to source
|
||||
embed.add_field(name="Source Code:", value="[R0b3Bot on Gitlab](https://gitlab.rickelobe.com/Bots/r0b3BOT)", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
bot.remove_command('help')
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
#@bot.command()
|
||||
#async def commands(ctx):
|
||||
# # Redirect to the 'help' command
|
||||
# await help(ctx)
|
||||
|
||||
@bot.command()
|
||||
async def help(ctx):
|
||||
embed = discord.Embed(title="R0b3BOT", description="List of commands are:", color=0xeee657)
|
||||
embed.add_field(name="$printstat", value="Uploads a snapshot of Rich's 3D printer and current stats", inline=False)
|
||||
embed.add_field(name="$explain", value="Displays Dalek EXPLAIN gif", inline=False)
|
||||
embed.add_field(name="$holyshit", value="Displays Marty McFly HOLY SHIT gif", inline=False)
|
||||
embed.add_field(name="$greetings", value="Gives a nice greet message", inline=False)
|
||||
embed.add_field(name="$cat", value="Gives a cute cat gif to lighten up the mood.", inline=False)
|
||||
embed.add_field(name="$bitch @member", value="Plays clip of BigEric420 saying 'maybe you shouldn't be such a bitch.mp3'", inline=False)
|
||||
embed.add_field(name="$boom @member", value="Plays 'Boom Bitch' sound clip", inline=False)
|
||||
embed.add_field(name="$cowbell @member", value="Plays 'SNL More Cowbell' sound clip", inline=False)
|
||||
embed.add_field(name="$oops @member", value="Plays 'Oops.mp3'", inline=False)
|
||||
embed.add_field(name="$promoted @member", value="Plays Battlefield Friends 'PROMOTED!' sound clip", inline=False)
|
||||
embed.add_field(name="$trololo @member", value="Plays a clip of Trololo song", inline=False)
|
||||
embed.add_field(name="$leeroy @member", value="Plays Leeeerrroooyyy Jenkins clip", inline=False)
|
||||
embed.add_field(name="$eia @member", value="Plays 'Everything is Awesome' song clip")
|
||||
embed.add_field(name="$info", value="Gives a little info about the bot", inline=False)
|
||||
embed.add_field(name="$help", value="Gives this message", inline=False)
|
||||
embed.add_field(name="$last_error", value="Will display the real error message the bot has last encountered for additional debugging info")
|
||||
embed.add_field(name="$uptime", value="Displays how long the bot has been up since last service restart")
|
||||
embed = discord.Embed(title='Core & Debug Commands:', description="Core commands for info and debug", color=0xeee657)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}info", value="Gives a little info about the bot", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}help", value="Gives this message", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}last_error", value="Will display the real error message the bot has last encountered for additional debugging info")
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}greetings", value="Gives a nice greeting message", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
embed = discord.Embed(title="A note about sound clips:", description="Sound clips are only played voice channels. If no user is specified when calling the command, the sound will played in the channel the user is currently joined to. When a username is specified, the bot will play the sound in the channel that user is currently in.")
|
||||
embed = discord.Embed(title="GIF Commands", description="Commands to call up a GIF image", color=0xeee657)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}explain", value="Displays Dalek EXPLAIN gif", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}holyshit", value="Displays Marty McFly HOLY SHIT gif", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}cat", value="Gives a cute cat gif to lighten up the mood.", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
embed = discord.Embed(title="Sound Commands", description="To play a sound in a chat channel, use one of the following:", color=0xeee657)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}aww @member", value="Plays Rick & Morty clip 'Awww bitch'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}bitch @member", value="Plays clip of BigEric420 saying 'maybe you shouldn't be such a bitch.mp3'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}big @member", value="Plays 'Wiz Khalifa clip'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}boom @member", value="Plays 'Boom Bitch' sound clip", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}breee @member", value="Plays 'BREEEEE' clip from a WBTBWB song", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}cowbell @member", value="Plays 'SNL More Cowbell' sound clip", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}eia @member", value="Plays 'Everything is Awesome' song clip", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}heavy @member", value="Plays 'Poppy ooo heavy'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}iran @member", value="Plays Trump 'I Ran'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}leeroy @member", value="Plays Leeeerrroooyyy Jenkins clip", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}nice @member", value="Plays clip from WBTBWB - Nice!", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}oops @member", value="Plays 'Oops.mp3'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}promoted @member", value="Plays Battlefield Friends 'PROMOTED!' sound clip", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}trololo @member", value="Plays a clip of Trololo song", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}wow @member", value="Plays clip of Owen Wilson saying 'Woooow'", inline=False)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}yaw @member", value="Plays clip of Busta Ryhmes 'Yaw Yaw Yaw'", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
embed = discord.Embed(title="A note about sound clips:", description="Sound clips are only played in voice channels. If a user is not specified when calling the command, the sound will played in the channel of that issuing user is currently joined to. When a username is specified, the bot will play the sound in the channel that user is currently in.")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
embed = discord.Embed(title="3D Printer Status", color=0xeee657)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}printstat", value="Uploads a snapshot of Rich's OctoPrint 3D printer and current stats", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
embed = discord.Embed(title="Bambu Lab Printer Status", color=0x00B4D8)
|
||||
embed.add_field(name=f"{BOT_COMMAND_PREFIX}bambustat", value="Displays the current status of the Bambu Lab printer (temps, progress, time remaining)", inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
# Function to run shell command and return stdout or sterr
|
||||
async def run(cmd):
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE)
|
||||
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
print(f'[{cmd!r} exited with {proc.returncode}]')
|
||||
if stdout:
|
||||
#print(f'[stdout]\n{stdout.decode()}')
|
||||
return stdout.decode()
|
||||
if stderr:
|
||||
#print(f'[stderr]\n{stderr.decode()}')
|
||||
return stderr.decode()
|
||||
|
||||
bot.run(DISCORD_AUTH_TOKEN)
|
||||
@@ -0,0 +1,13 @@
|
||||
# r0b3BOT Python dependencies
|
||||
# Install with: pip install -r requirements.txt
|
||||
|
||||
# Discord bot framework — [voice] pulls in PyNaCl for voice channel encryption
|
||||
discord.py[voice]>=2.6.4
|
||||
|
||||
# HTTP client — used for HomeAssistant REST API calls
|
||||
requests>=2.32.3
|
||||
|
||||
# Bambu Lab printer integration via local MQTT
|
||||
# bambu-connect 0.3.1 uses mqtt.CallbackAPIVersion.VERSION1, which requires paho-mqtt 2.x
|
||||
bambu-connect>=0.3.1
|
||||
paho-mqtt>=2.0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user