diff --git a/main.py b/main.py index 0363be2..4ddc772 100755 --- a/main.py +++ b/main.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 -# Bitlair HobbyBot - -from time import sleep -from discord import Intents -from discord.ext import commands -from discord_webhook import DiscordWebhook, DiscordEmbed -import pytz -import paho.mqtt.client as mqtt -import paho.mqtt.subscribe as subscribe +import asyncio import os import sys +from time import sleep +import aiomqtt +import pytz +from discord import Intents +from discord.ext import commands +from discord_webhook import DiscordEmbed, DiscordWebhook mqtt_host = os.getenv("MQTT_HOST") if not mqtt_host: @@ -37,13 +35,11 @@ intents.members = True HobbyBot = commands.Bot(command_prefix="!", description="Bitlair Bot", intents=intents) -def mqtt_get_one(topic): - try: - msg = subscribe.simple(topic, hostname=mqtt_host, keepalive=10) - return msg.payload.decode() - except Exception as err: - print(err) - return "" +async def mqtt_get_one(topic, timeout=20): + async with asyncio.timeout(timeout): + async with aiomqtt.Client(mqtt_host) as mq: + await mq.subscribe(topic) + return await anext(mq.messages) # Define bot commands @@ -56,35 +52,52 @@ async def on_ready(): @HobbyBot.command(description="Bitlair Space State") async def state(ctx): async with ctx.typing(): - spaceState = mqtt_get_one("bitlair/state") - if spaceState == "open": - await ctx.send("Bitlair is OPEN! :sunglasses:") - elif spaceState == "closed": - await ctx.send("Bitlair is closed :pensive:") + try: + msg = await mqtt_get_one("bitlair/state") + space_state = msg.payload.decode("ascii") + if space_state == "open": + await ctx.send("Bitlair is OPEN! :sunglasses:") + elif space_state == "closed": + await ctx.send("Bitlair is closed :pensive:") + except Exception as err: + await ctx.send("Meh, stuk") + raise err # !co2 @HobbyBot.command(description="co2 levels") async def co2(ctx): async with ctx.typing(): - hoofdruimte = mqtt_get_one("bitlair/climate/hoofdruimte_ingang/co2_ppm") - await ctx.send("Hoofdruimte: %s ppm\n" % hoofdruimte) + try: + msg = await mqtt_get_one("bitlair/climate/hoofdruimte/co2_ppm") + await ctx.send(f"Hoofdruimte: {msg.payload.decode('ascii')} ppm\n") + except Exception as err: + await ctx.send("Meh, stuk") + raise err # !temp @HobbyBot.command(description="Temperature") async def temp(ctx): async with ctx.typing(): - hoofdruimte = mqtt_get_one("bitlair/climate/hoofdruimte_ingang/temperature_c") - await ctx.send("Hoofdruimte: %s °C\n" % hoofdruimte) + try: + msg = await mqtt_get_one("bitlair/climate/hoofdruimte/temperature_c") + await ctx.send(f"Hoofdruimte: {msg.payload.decode('ascii')} °C\n") + except Exception as err: + await ctx.send("Meh, stuk") + raise err # !humid @HobbyBot.command(description="Humidity") async def humid(ctx): async with ctx.typing(): - hoofdruimte = mqtt_get_one("bitlair/climate/hoofdruimte_ingang/humidity_pct") - await ctx.send("Hoofdruimte: %s pct\n" % hoofdruimte) + try: + msg = await mqtt_get_one("bitlair/climate/hoofdruimte/humidity_pct") + await ctx.send(f"Hoofdruimte: {msg.payload.decode('ascii')} pct\n") + except Exception as err: + await ctx.send("Meh, stuk") + raise err # !np @@ -94,69 +107,51 @@ async def np(ctx): await ctx.send("Now playing: Darude - Sandstorm") -# define mqtt client stuff -# -# subscribe to topics -def on_connect(client, userdata, flags, rc): - client.subscribe("bitlair/alarm") - client.subscribe("bitlair/state") - client.subscribe("bitlair/state/djo") - client.subscribe("bitlair/photos") - - def webhook_message(msg): webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True, content=msg) webhook.execute() -retained = { - "bitlair/alarm", - "bitlair/photos", - "bitlair/state", - "bitlair/state/djo", -} +async def event_task(): + retained = { + "bitlair/alarm", + "bitlair/photos", + "bitlair/state", + "bitlair/state/djo", + } + + async with aiomqtt.Client(mqtt_host) as mq: + await asyncio.gather(*[mq.subscribe(topic) for topic in retained]) + async for msg in mq.messages: + # Retained messages trigger an initial message on connecting. Prevent relaying them to Discord on startup. + if str(msg.topic) in retained: + retained.remove(str(msg.topic)) + continue + + payload = msg.payload.decode("ascii") + + if msg.topic.matches("bitlair/alarm"): + webhook_message(f"Alarm: {payload}") + elif msg.topic.matches("bitlair/state"): + webhook_message(f"Bitlair is now {payload.upper()}") + elif msg.topic.matches("bitlair/state/djo"): + webhook_message(f"DJO is now {payload.upper()}") + elif msg.topic.matches("bitlair/photos"): + webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True) + embed = DiscordEmbed(title="WIP Cam", color="fc5d1d") + embed.set_url(f"https://bitlair.nl/fotos/view/{payload}") + embed.set_image(f"https://bitlair.nl/fotos/photos/{payload}") + webhook.add_embed(embed) + webhook.execute() + else: + continue + sleep(1) # Prevent triggering rate limits. -# post to mqtt discord channel when state changes -def on_message(client, userdata, msg): - try: - topic = msg.topic - msg = msg.payload.decode() - - # Retained messages trigger an initial message on connecting. Prevent relaying them to - # Discord on startup. - if topic in retained: - retained.remove(topic) - return - - if topic == "bitlair/alarm": - webhook_message("Alarm: %s" % msg) - elif topic == "bitlair/state": - webhook_message("Bitlair is now %s" % msg.upper()) - elif topic == "bitlair/state/djo": - webhook_message("DJO is now %s" % msg.upper()) - elif topic == "bitlair/photos": - webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True) - embed = DiscordEmbed(title="WIP Cam", color="fc5d1d") - embed.set_url("https://bitlair.nl/fotos/view/" + msg) - embed.set_image("https://bitlair.nl/fotos/photos/" + msg) - webhook.add_embed(embed) - webhook.execute() - else: - return - sleep(1) # Prevent triggering rate limits. - except Exception as e: - print(e) +async def main(): + t1 = asyncio.create_task(HobbyBot.start(token)) + t2 = asyncio.create_task(event_task()) + await asyncio.gather(t1, t2) -client = mqtt.Client() -client.on_connect = on_connect -client.on_message = on_message -client.connect(mqtt_host, 1883, 60) - -# Start mqtt loop and discord bot -client.loop_start() -HobbyBot.run(token) - -# Exit when bot crashes -client.loop_stop(force=True) +asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 90f95ff..cc655ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ description = "Bitlair Discord Bot" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "aiomqtt>=2.4.0", "discord-py>=2.5.2", "discord-webhook>=1.4.1", - "paho-mqtt>=2.1.0", "pytz>=2025.2", ] diff --git a/uv.lock b/uv.lock index c1fd345..173362c 100644 --- a/uv.lock +++ b/uv.lock @@ -64,6 +64,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/3c/143831b32cd23b5263a995b2a1794e10aa42f8a895aae5074c20fda36c07/aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01", size = 437658, upload-time = "2025-04-21T09:42:29.209Z" }, ] +[[package]] +name = "aiomqtt" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "paho-mqtt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/9a/863bc34c64bc4acb9720a9950bfc77d6f324640cdf1f420bb5d9ee624975/aiomqtt-2.4.0.tar.gz", hash = "sha256:ab0f18fc5b7ffaa57451c407417d674db837b00a9c7d953cccd02be64f046c17", size = 82718, upload-time = "2025-05-03T20:21:27.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/0c/2720665998d97d3a9521c03b138a22247e035ba54c4738e934da33c68699/aiomqtt-2.4.0-py3-none-any.whl", hash = "sha256:721296e2b79df5f6c7c4dfc91700ae0166953a4127735c92637859619dbd84e4", size = 15908, upload-time = "2025-05-03T20:21:26.337Z" }, +] + [[package]] name = "aiosignal" version = "1.3.2" @@ -174,17 +186,17 @@ name = "discord-bot" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "aiomqtt" }, { name = "discord-py" }, { name = "discord-webhook" }, - { name = "paho-mqtt" }, { name = "pytz" }, ] [package.metadata] requires-dist = [ + { name = "aiomqtt", specifier = ">=2.4.0" }, { name = "discord-py", specifier = ">=2.5.2" }, { name = "discord-webhook", specifier = ">=1.4.1" }, - { name = "paho-mqtt", specifier = ">=2.1.0" }, { name = "pytz", specifier = ">=2025.2" }, ]