You’re probably here because you saw this video below and want quick and dirty technical info, and I have that for you, dear reader.


First off, the 3D CAD files. This took quite some time to get right, but I’m happy with how they work:


Here’s hopefully everything you need to upload to print your own PCB’s:


The code is in micropython and written to work with Indigo (my home automation platform). I’m sure it can easily be modified for Home Assistant. There are 4 files:

  • config.py simply contains info to customize the units for each recipient (it will need modification)
  • dfplayermini.py allows control of the micro sd card
  • indigoFunc.py is purely a way to separate Indigo-specific functions from the main code
  • mani.py is the main loop

config.py

WIFI_SSID = 'YOUR WIFI'
WIFI_PASSWORD = 'YOUR WIFI PASSWORD'
OWNER = 'Dan'
OWNER_ID = <<ID FOR SERVER VARIABLE>>

indigoFunc.py

#CODE BELOW IS IN MICROPYTHON

import urequests
import ujson as json

REFLECTORNAME = "USERNAME FOR INDIGO"
APIKEY = "API KEY"
VARIABLEID = VARIABLE ID USED FOR TESTING  # Indigo variable id

def updateVariable(varID, value):
    # The message to send to the Indigo Server
    message = json.dumps({
        "id": "optional-user-generated-id",
        "message": "indigo.variable.updateValue",
        "objectId": varID,
        "parameters": {
            "value": value
        }
    })
    headers = {'Authorization': 'Bearer %s' % APIKEY}
    url = 'https://%s.indigodomo.net/v2/api/command' % REFLECTORNAME
    response = urequests.post(url, data=message, headers=headers)
    reply = response.json()
    #print(reply)
    return

def readVariable(varID):
    headers = {'Authorization': 'Bearer %s' % APIKEY}
    url = 'https://%s.indigodomo.net/v2/api/indigo.variables/%s' % (REFLECTORNAME, varID)
    response = urequests.get(url, headers=headers)
    var_instance = response.json()
    #print(var_instance)
    return var_instance['value']

def main():
    print("Running main")
    # usage
    # updateVariable(VARIABLEID, "True")
    # readVariable(VARIABLEID)

if __name__ == "__main__":
    main()

main.py

import time
import machine
import network
from machine import Pin, Timer
import urequests
from indigoFunc import *
from dfplayermini import Player
import config
import urandom

isPartyID = VARIABLE ID THAT STORES THE STATUS OF ISPARTY
overrideHostID = TOO COMPLICATED TO EXPLAIN
ownerResponded = False
alertTimeout = 1
numAlerts = 0

#introMusic = 1
#yes responses = 2-25
#maybe = 26-30
#no = 31

music = Player(pin_TX=17, pin_RX=16)
music.volume(14)
led = machine.Pin(2, machine.Pin.OUT)

class LastSeven:
    def __init__(self):
        self.integers = []

    def add_integer(self, num):
        self.integers.append(num)
        while len(self.integers) > 5:
            self.integers.pop(0)
        return self.check_sequence()

    def check_sequence(self):
        return self.integers == [3, 2, 1, 3, 2]

checker = LastSeven()

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
    print('Connecting to network...')
    wlan.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
    while not wlan.isconnected():
        pass
print('Network connected:', wlan.ifconfig())

# setup the buttons on GPIO 13, 14, and 15
button1 = Pin(13, Pin.IN, Pin.PULL_UP)
button2 = Pin(12, Pin.IN, Pin.PULL_UP)
button3 = Pin(14, Pin.IN, Pin.PULL_UP)

debounce_time = 1000 # in milliseconds
last_press_time = {13: 0, 12: 0, 14: 0}

def button1_callback(p):
    global last_press_time
    global ownerResponded
    global checker
    now = time.ticks_ms()
    if now - last_press_time[13] < debounce_time:
        return
    last_press_time[13] = now
    this = checker.add_integer(1)
    print(this)
    print('Button 1 pressed')
    randomVal = urandom.randint(2, 25) 
    print(f"Random value of {randomVal}")
    music.play(randomVal)
    if (ownerResponded==False):
        ownerResponded=True    
        turnOff()
        print(f"{config.OWNER} responded {ownerResponded}")
        updateVariable(config.OWNER_ID, "yes")
    return

def button2_callback(p):
    global last_press_time
    global ownerResponded
    global checker
    now = time.ticks_ms()
    if now - last_press_time[12] < debounce_time:
        return
    last_press_time[12] = now
    this = checker.add_integer(2)
    print(this)
    print('Button 2 pressed')
    randomVal = urandom.randint(26, 30) 
    print(f"Random value of {randomVal}")
    music.play(randomVal)
    if (ownerResponded==False):
        updateVariable(config.OWNER_ID, "maybe")
        turnOff()
        print(f"{config.OWNER} responded {ownerResponded}")
        ownerResponded=True
    print(type(this))
    if (this==True):
        #this part is if the person who wants to host is triggering it
        updateVariable(overrideHostID, f"{config.OWNER_ID}")
        updateVariable(isPartyID, "Override")
        ownerResponded == True
        updateVariable(config.OWNER_ID, "yes")
        partyOut(config.OWNER_ID)
    return

def button3_callback(p):
    global last_press_time
    global ownerResponded
    global checker
    now = time.ticks_ms()
    if now - last_press_time[14] < debounce_time:
        return
    last_press_time[14] = now
    this = checker.add_integer(3)
    print(this)
    print('Button 3 pressed')
    music.play(31)
    if (ownerResponded==False):
        updateVariable(config.OWNER_ID, "no")
        turnOff()
        print(f"{config.OWNER} responded {ownerResponded}")
        ownerResponded=True
    return

# set the interrupt
button1.irq(trigger=Pin.IRQ_FALLING, handler=button1_callback)
button2.irq(trigger=Pin.IRQ_FALLING, handler=button2_callback)
button3.irq(trigger=Pin.IRQ_FALLING, handler=button3_callback)

def partyOn():
    global numAlerts
    print("***function partyOn started...")
    led.on()
    print("Start Music!!!")
    music.play(1)
    numAlerts+=1
    return

def partyOut(hostId): #this is for the override function
    global numAlerts
    print("***system override message...")
    print("Start Party Out Announcement!!!")
    music.play(32) #this is the override audio
    numAlerts+=1
    return

def turnOff():
    print("***function turnOff started...")
    led.off()
    return

def partyOff():
    global ownerResponded
    global numAlerts
    print("***function partyOff started...")
    led.off()
    ownerResponded=False
    numAlerts = 0
    return

def check_status(t):
    global ownerResponded
    global alertTimeout
    global numAlerts
    global overrideHostID
    print("***fuction check_status begun")
    resp = readVariable(isPartyID)
    print("read variable")
    print(resp)
    print(type(resp))
    if resp=="False":
        partyOff()
        return
    if(ownerResponded or numAlerts > alertTimeout):
        return
    if resp=="True":
        partyOn()
    if resp=="Override":
        #check the value of indigo overrideHostID
        print("***checking the value of overrideHost")
        resp = readVariable(overrideHostID)
        #call a function similar to partyOn, called partyOut
        partyOut(resp)
    print(f"{config.OWNER} responded {ownerResponded}")

# create a timer object7
timer = Timer(-1)
# schedule the timer to call check_status every minute
timer.init(period=60000, mode=Timer.PERIODIC, callback=check_status)

check_status(timer)

# main loop does nothing, all work is done in interrupts now
while True:
    pass

dfplayermini.py was written and hosted at https://github.com/lavron/micropython-dfplayermini


Almost forgot the materials and links…

Spastix https://amzn.to/3Rg7AY8

Rubber Feet https://amzn.to/40TrTOi

6N138 Optoisolartors https://amzn.to/3QNCjdK

Esp32 https://amzn.to/3GiCbya

Hiletgo mp3 Player https://amzn.to/47QS8XQ

MT3608 Boost Converter https://amzn.to/49UexFK

12V LEDs https://amzn.to/40WVq9U

Gikfun Speaker https://amzn.to/3MVlAEk

50mm diameter 40mm focal fresnel lens https://amzn.to/3uzQHPl


This is rushed and sloppy, but I want to get the video up and i needed to post the resources first. Thanks for making it this far!