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 | ||
|
|
dca02881c8 | ||
|
|
fabd6efe69 | ||
|
|
fe2d9c21d3 | ||
|
|
582a5c05e6 | ||
|
|
1be37c1b4d | ||
|
|
f45e3d654a | ||
|
|
8204d41193 | ||
|
|
5f3a1a9f3f | ||
|
|
13d9bad9e6 | ||
|
|
1c7e26b26e | ||
|
|
5cc9d98475 | ||
|
|
46d3b7b95c | ||
|
|
13ec709616 | ||
|
|
f1dc6e2975 | ||
|
|
4cf2cb0864 | ||
|
|
d34a8e5387 | ||
|
|
8814ac1013 | ||
|
|
e3c197e22b | ||
|
|
efbdcace68 | ||
|
|
23c0343917 | ||
|
|
e9a72ae15c | ||
|
|
ef77651512 | ||
|
|
e62bc7ea36 | ||
|
|
bc10fac9f0 | ||
|
|
81deee6692 | ||
|
|
b44075c37d | ||
|
|
99a6d362d5 | ||
|
|
4bd4bf56d3 | ||
|
|
9be971ce64 | ||
|
|
efc899be71 | ||
|
|
277c3b3b36 | ||
|
|
e4c69be8f2 | ||
|
|
1fe66ee2da | ||
|
|
96ea3cf3f3 | ||
|
|
503117f242 | ||
|
|
57bd92cc7d | ||
|
|
298eed22cc | ||
|
|
5005602292 | ||
|
|
66472f0f06 | ||
|
|
2a79e312be | ||
|
|
248e598f77 | ||
|
|
bb265b750e | ||
|
|
97a71f2bda |
No files matched your search
@@ -1 +1,3 @@
|
||||
.vscode/launch.json
|
||||
__pycache__
|
||||
bot_config.conf
|
||||
@@ -4,3 +4,6 @@ stages:
|
||||
deploy:
|
||||
stage: deploy
|
||||
script: /var/lib/gitlab-runner/deploy_r0b3bot.sh
|
||||
only:
|
||||
- dev
|
||||
- master
|
||||
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+495
-94
@@ -1,23 +1,42 @@
|
||||
#!/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()
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
configFilePath = r'bot_config.conf'
|
||||
@@ -27,7 +46,18 @@ try:
|
||||
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')
|
||||
@@ -36,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!")
|
||||
@@ -65,11 +231,13 @@ 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}")
|
||||
await ctx.send("Error, unable to complete your request.")
|
||||
@@ -77,6 +245,12 @@ async def on_command_error(ctx, error):
|
||||
print('Oopsie, I found an error...')
|
||||
print(f"Error: {error}")
|
||||
|
||||
#
|
||||
# Debug commands
|
||||
#
|
||||
@bot.command()
|
||||
async def last_error(ctx):
|
||||
await ctx.send(f"Last error recorded: {last_error_msg}")
|
||||
|
||||
#
|
||||
# Giphy Commands
|
||||
@@ -95,10 +269,18 @@ async def holyshit(ctx):
|
||||
|
||||
###
|
||||
|
||||
#
|
||||
# Image Commands
|
||||
#
|
||||
@bot.command()
|
||||
async def smudge(ctx):
|
||||
print(" - Uploading Smudge to chat channel")
|
||||
file = discord.File("./images/smudge.png", filename="smudge.png")
|
||||
await ctx.send(file=file)
|
||||
|
||||
#
|
||||
# Voice Commands
|
||||
#
|
||||
|
||||
@bot.command()
|
||||
@commands.cooldown(rate=1, per=30.0, type=commands.BucketType.user)
|
||||
async def bitch(ctx, member : discord.Member="NONE"):
|
||||
@@ -151,6 +333,50 @@ async def promoted(ctx, member : discord.Member="NONE"):
|
||||
async def wow(ctx, member : discord.Member="NONE"):
|
||||
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")
|
||||
|
||||
@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")
|
||||
|
||||
@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)
|
||||
async def rs(ctx, member : discord.Member="NONE"):
|
||||
@@ -165,8 +391,6 @@ async def rs(ctx, member : discord.Member="NONE"):
|
||||
|
||||
async def play_sound(ctx, member : discord.Member, soundFile, command):
|
||||
|
||||
discord.opus.load_opus("libopus.so")
|
||||
|
||||
datestring = datetime.now()
|
||||
datestring = datestring.strftime("%m/%d/%Y-%H:%M:%S")
|
||||
|
||||
@@ -177,7 +401,7 @@ async def play_sound(ctx, member : discord.Member, soundFile, command):
|
||||
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
|
||||
@@ -187,7 +411,7 @@ async def play_sound(ctx, member : discord.Member, soundFile, command):
|
||||
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
|
||||
@@ -205,13 +429,17 @@ async def play_sound(ctx, member : discord.Member, soundFile, command):
|
||||
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}")
|
||||
await asyncio.sleep(1)
|
||||
voice.play(audio_source, after=None)
|
||||
time.sleep(5)
|
||||
while voice.is_playing():
|
||||
await asyncio.sleep(1)
|
||||
print(f" - Disconnecting from '{channel}'")
|
||||
await voice.disconnect()
|
||||
###
|
||||
@@ -227,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]")
|
||||
|
||||
@@ -267,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)
|
||||
@@ -287,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:
|
||||
@@ -296,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}")
|
||||
@@ -316,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)
|
||||
|
||||
@@ -393,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")
|
||||
@@ -408,22 +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 = "Unknown"
|
||||
unknowns += 1
|
||||
|
||||
print_completion = round(await loop.run_in_executor(None, octoapi.get_completion), 1)
|
||||
except Exception:
|
||||
print_completion = 999
|
||||
|
||||
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
|
||||
activity = discord.Activity(name=f"3D Print @ {str(print_completion)}%",type=discord.ActivityType.watching)
|
||||
if print_completion != 999:
|
||||
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)
|
||||
|
||||
@@ -439,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):
|
||||
@@ -456,28 +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 'monkey_bitch.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$boom @member", value="Plays 'BoomBitch.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$cowbell @member", value="Plays 'More_cowbell.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$oops @member", value="Plays 'Oops.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$promoted @member", value="Plays 'Promoted.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$trololo @member", value="Plays 'Trololo.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
embed.add_field(name="$leeroy @member", value="Plays 'leeroy.mp3' to the users voice channel, if no user is specified it will play in your own voice channel", inline=False)
|
||||
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 = 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)
|
||||
|
||||
bot.run(DISCORD_AUTH_TOKEN)
|
||||
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.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user