Bar-Shop Bat-Signals

November 25, 2023

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

Rubber Feet

6N138 Optoisolators

ESP32

Hiletgo mp3 Player

MT3608 Boost Converter

12V LEDs

Gikfun Speaker

50mm condenser lens (the original fresnel listing is gone — this is a close substitute)

The Amazon links above are affiliate links — as an Amazon Associate I earn from qualifying purchases.


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!