#!/usr/bin/env python3 import asyncio import os import sys from time import sleep import aiomqtt import pytz from discord import File, Intents from discord.ext import commands from discord_webhook import DiscordEmbed, DiscordWebhook import bottleclip as clip mqtt_host = os.getenv("MQTT_HOST") if not mqtt_host: print("MQTT_HOST unset") sys.exit(1) token = os.getenv("DISCORD_TOKEN") if not token: print("DISCORD_TOKEN unset") sys.exit(1) webhook_url = os.getenv("DISCORD_WEBHOOK_URL") if not webhook_url: print("DISCORD_WEBHOOK_URL unset") sys.exit(1) timezone = pytz.timezone("Europe/Amsterdam") # Discord bot stuff intents = Intents.default() intents.message_content = True intents.members = True HobbyBot = commands.Bot(command_prefix="!", description="Bitlair Bot", intents=intents) 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 @HobbyBot.event async def on_ready(): print(f"Logged in as {HobbyBot.user} (ID: {HobbyBot.user.id})") # !state @HobbyBot.command(description="Bitlair Space State") async def state(ctx): async with ctx.typing(): try: msg = await mqtt_get_one("bitlair/state") space_state = msg.payload.decode("ascii") if space_state == "open": await ctx.reply("Bitlair is OPEN! :sunglasses:") elif space_state == "closed": await ctx.reply("Bitlair is closed :pensive:") except Exception as err: await ctx.reply("Meh, stuk") raise err # !co2 @HobbyBot.command(description="co2 levels") async def co2(ctx, where="hoofdruimte"): async with ctx.typing(): try: msg = await mqtt_get_one(f"bitlair/climate/{where}/co2_ppm") await ctx.reply(f"{where}: {msg.payload.decode('ascii')} ppm\n") except Exception as err: await ctx.reply("Meh, stuk") raise err # !temp @HobbyBot.command(description="Temperature") async def temp(ctx, where="hoofdruimte"): async with ctx.typing(): try: msg = await mqtt_get_one(f"bitlair/climate/{where}/temperature_c") await ctx.reply(f"{where}: {msg.payload.decode('ascii')} °C\n") except Exception as err: await ctx.reply("Meh, stuk") raise err # !humid @HobbyBot.command(description="Humidity") async def humid(ctx, where="hoofdruimte"): async with ctx.typing(): try: msg = await mqtt_get_one(f"bitlair/climate/{where}/humidity_pct") await ctx.reply(f"{where}: {msg.payload.decode('ascii')} pct\n") except Exception as err: await ctx.reply("Meh, stuk") raise err # !np @HobbyBot.command(description="Now Playing") async def np(ctx): async with ctx.typing(): await ctx.reply("Now playing: Darude - Sandstorm") # !bottleclip @HobbyBot.command(description="Generate a bottle-clip STL file suitable for printing") async def bottleclip(ctx, icon: str = "", ears: bool = False): icons = clip.list_icons() if icon not in icons: await ctx.reply( f"usage: `!bottleclip []`\n* `icon` must be one of {', '.join(icons)}" ) return async with ctx.typing(): label = ctx.author.nick or ctx.author.global_name stl_file = clip.create_stl(label, icon, ears) attach = File(stl_file.name, filename=f"{label}.stl") await ctx.reply("Ok! Hier is je flessenclip", file=attach) def webhook_message(msg): webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True, content=msg) webhook.execute() 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. async def main(): t1 = asyncio.create_task(HobbyBot.start(token)) t2 = asyncio.create_task(event_task()) await asyncio.gather(t1, t2) asyncio.run(main())