Separate Discord API and command implementations

This commit is contained in:
polyfloyd 2025-05-16 20:04:18 +02:00
parent 677d4f1259
commit 78f30b19fe
3 changed files with 109 additions and 90 deletions

118
commands/__init__.py Normal file
View file

@ -0,0 +1,118 @@
import asyncio
import aiomqtt
from discord_webhook import DiscordEmbed
import commands.bottleclip as bottleclip
_mqtt_host = None
def setup(bot, mqtt_host):
global _mqtt_host
_mqtt_host = mqtt_host
# !state
bot.command(description="Bitlair Space State")(state)
# !co2
bot.command(description="co2 levels")(co2)
# !temp
bot.command(description="Temperature")(temp)
# !humid
bot.command(description="Humidity")(humid)
# !np
bot.command(description="Now Playing")(np)
# !bottleclip
bot.command(
name="bottleclip",
description="Generate a bottle-clip STL file suitable for printing",
)(bottleclip.command)
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)
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
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
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
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
async def np(ctx):
async with ctx.typing():
await ctx.reply("Now playing: Darude - Sandstorm")
async def run_events(mqtt_host):
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"):
yield f"Alarm: {payload}"
elif msg.topic.matches("bitlair/state"):
yield f"Bitlair is now {payload.upper()}"
elif msg.topic.matches("bitlair/state/djo"):
yield f"DJO is now {payload.upper()}"
elif msg.topic.matches("bitlair/photos"):
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}")
yield embed
else:
continue

64
commands/bottleclip.py Normal file
View file

@ -0,0 +1,64 @@
import os
import subprocess
from os.path import abspath, join, splitext
from tempfile import NamedTemporaryFile
from typing import List
from discord import File
def resource_dir() -> str:
p = os.getenv("BOTTLECLIP_RESOURCES")
assert p is not None
return p
def list_icons() -> List[str]:
files = os.listdir(join(resource_dir(), "icons"))
return sorted(set(files) - {"README.md"})
def create_stl(label: str, icon: str, ears: bool) -> NamedTemporaryFile:
icon_path = abspath(join(resource_dir(), "icons", icon))
ears_str = "true" if ears else "false"
font_path = abspath(join(resource_dir(), "write/orbitron.dxf"))
scad = NamedTemporaryFile(suffix=".scad")
with open(join(resource_dir(), "bottle-clip.scad"), "rb") as f:
scad.write(f.read())
scad.write(b"\n\n")
scad.write(
f'bottle_clip(name="{label}", logo="{icon_path}", ears={ears_str}, font="{font_path}");'.encode(
"utf-8"
)
)
scad.flush()
stl = NamedTemporaryFile(suffix=".scad")
subprocess.run(
["openscad", scad.name, "--export-format", "binstl", "-o", stl.name],
env={
"OPENSCADPATH": resource_dir(),
},
)
stl.seek(0)
return stl
async def command(ctx, icon: str = "", ears: bool = False):
icons = list_icons()
if icon not in icons:
await ctx.reply(
f"usage: `!bottleclip <icon> [<ears y|n>]`\n* `icon` must be one of {', '.join(icons)}"
)
return
async with ctx.typing():
label = ctx.author.nick or ctx.author.global_name
stl_file = create_stl(label, icon, ears)
icon_name, _ = splitext(icon)
with_ears = "_ears" if ears else ""
attach = File(stl_file.name, filename=f"{label}_{icon_name}{with_ears}.stl")
await ctx.reply("Ok! Hier is je flessenclip", file=attach)