Issue with SmartApp Requiring Reinstallation for server.post to be Recognized

Hello,

I’ve been working on a project that involves detecting a smoke alarm through audio signal processing. Once detected, my Python script sends a POST request to a Node.js application hosted on Glitch, which should then communicate with the SmartThings platform to activate a virtual switch.

However, I’ve encountered a problem. After a while, the POST request from my Python script receives a 404 error. The issue is resolved if I delete and reinstall the SmartApp on SmartThings. I suspect the problem might be in the server.js file I crudely adapted from the basic SmartApp example.

Here’s the server.js:

// server.js
‘use strict’;

const express = require(‘express’);
const bodyParser = require(‘body-parser’);
const SmartApp = require(‘@smartthings/smartapp’);

const server = module.exports = express();
server.use(bodyParser.json());

const app = new SmartApp()

/* Only here for Glitch, so that GET doesn’t return an error */
server.get(‘/’, (req, res) => {
res.send(‘Smoke Alarm App: https://’ + req.hostname);
});

/* Handles lifecycle events from SmartThings */
server.post(‘/’, async (req, res) => {
app.handleHttpCallback(req, res);
});

/* Defines the SmartApp */
app.enableEventLogging() // Log and pretty-print all lifecycle events and responses
.configureI18n() // Use files from locales directory for configuration page localization
.page(‘mainPage’, (context, page, configData) => {
page.section(‘lights’, section => {
section.deviceSetting(‘lights’).capabilities([‘switch’]).multiple(true).permissions(‘rx’);
});
})
.updated(async (context, updateData) => {
await context.api.subscriptions.unsubscribeAll();

// Define /alarm endpoint within updated method to have access to context
server.post('/alarm', async (req, res) => {
  const alarmStatus = req.body.status;  // Assuming the status is sent in the request body

  if (alarmStatus === 'Alarm!') {
    // Send a command to toggle the virtual switch on
    await context.api.devices.sendCommands(context.config.lights, 'switch', 'off');
    await context.api.devices.sendCommands(context.config.lights, 'switch', 'on');
    res.status(200).send('Smoke alarm alert received and processed');
  } else {
    res.status(400).send('Invalid alarm status');
  }
});

});

/* Starts the server */
let port = process.env.PORT;
server.listen(port);
console.log(Open: http://127.0.0.1:${port});

Also, if anyone is interested in the Python script:

import requests
import pyaudio
import pydub
import numpy as np
from numpy import log
from scipy.fft import fft
from time import sleep
import threading

Volume Sensitivity, 0.05: Extremely Sensitive, may give false alarms

0.1: Probably Ideal volume

1: Poorly sensitive, will only go off for relatively loud

SENSITIVITY = 1

Alarm frequency (Hz) to detect

TONE = 3100

Bandwidth for detection

BANDWIDTH = 30

How many 46ms blips before we declare a beep?

beeplength = 8

How many beeps before we declare an alarm?

alarmlength = 2

How many false 46ms blips before we declare there are no more beeps?

resetlength = 10

How many reset counts until we clear an active alarm?

clearlength = 1

Enable blip, beep, and reset debug output

debug = False

Show the most intense frequency detected

frequencyoutput = False

NUM_SAMPLES = 2048
SAMPLING_RATE = 44100

GLITCH_PROJECT_URL = ‘’

Whether to process audio files or live audio

PROCESS_AUDIO_FILES = False # Set to False to process live audio instead

Whether to prompt the user for the microphone device index

PROMPT_USER_FOR_DEVICE = False

if PROMPT_USER_FOR_DEVICE:
def list_microphones():
p = pyaudio.PyAudio()
info = p.get_host_api_info_by_index(0)
numdevices = info.get(‘deviceCount’)
for i in range(0, numdevices):
if p.get_device_info_by_host_api_device_index(0, i).get(‘maxInputChannels’) > 0:
print(f"Input Device id {i} - {p.get_device_info_by_host_api_device_index(0, i).get(‘name’)}")
p.terminate()

list_microphones()  # This will list all microphones
device_index = int(input("Enter the device index of the microphone you want to use: "))

else:
device_index = 1 # default device index

blipcount = 0
beepcount = 0
resetcount = 0
clearcount = 0
alarm = False

def keep_awake():
while True:
try:
response = requests.get(‘https://seasoned-nasal-slug.glitch.me’)
if response.status_code == 200:
print(f’Successfully pinged your Glitch project at {GLITCH_PROJECT_URL}‘)
else:
print(f’Failed to ping your Glitch project. HTTP Status Code: {response.status_code}’)
except requests.RequestException as e:
print(f’An error occurred: {e}')

    for i in range(240):  # prints a message every second for 4 minutes
        print(f'Running... {i + 1}')
        sleep(1)

def process_audio(audio_data):
global blipcount, beepcount, resetcount, clearcount, alarm
normalized_data = audio_data / 32768.0
intensity = abs(fft(normalized_data))[:len(normalized_data) // 2]
frequencies = np.linspace(0.0, float(SAMPLING_RATE) / 2, num=len(normalized_data) // 2)

if frequencyoutput:
    which = intensity[1:].argmax() + 1
    # use quadratic interpolation around the max
    if which != len(intensity) - 1:
        y0, y1, y2 = log(intensity[which - 1:which + 2])
        x1 = (y2 - y0) * .5 / (2 * y1 - y2 - y0)
        # find the frequency and output it
        thefreq = (which + x1) * SAMPLING_RATE / NUM_SAMPLES
    else:
        thefreq = which * SAMPLING_RATE / NUM_SAMPLES
    print(f"\t\t\t\tfreq={thefreq}")

if max(intensity[(frequencies < TONE + BANDWIDTH) & (frequencies > TONE - BANDWIDTH)]) > max(
        intensity[(frequencies < TONE - 1000) & (frequencies > TONE - 2000)]) + SENSITIVITY:
    blipcount += 1
    resetcount = 0
    if debug:
        print("\t\tBlip", blipcount)
    if blipcount >= beeplength:
        blipcount = 0
        resetcount = 0
        beepcount += 1
        if debug:
            print("\tBeep", beepcount)
        if beepcount >= alarmlength:
            clearcount = 0
            alarm = True
            print("Alarm!")
            beepcount = 0
else:
    blipcount = 0
    resetcount += 1
    if debug:
        print("\t\t\treset", resetcount)
    if resetcount >= resetlength:
        resetcount = 0
        beepcount = 0
        if alarm:
            clearcount += 1
            if debug:
                print("\t\tclear", clearcount)
            if clearcount >= clearlength:
                clearcount = 0
                print("Cleared alarm!")
                alarm = False

            response = requests.post('https://seasoned-nasal-slug.glitch.me/alarm', json={'status': 'Alarm!'})
            if response.status_code == 200:
                print('Successfully sent alarm alert to SmartApp')
            else:
                print(f'Failed to send alarm alert to SmartApp: {response.status_code}')

def process_live_audio():
pa = pyaudio.PyAudio()
_stream = pa.open(format=pyaudio.paInt16,
channels=1,
rate=SAMPLING_RATE,
input=True,
frames_per_buffer=NUM_SAMPLES,
input_device_index=device_index)

print("Alarm detector working. Press CTRL-C to quit.")

try:
    while True:
        try:
            while _stream.get_read_available() < NUM_SAMPLES:
                sleep(0.01)
            audio_data = np.frombuffer(_stream.read(_stream.get_read_available()), dtype=np.int16)
            process_audio(audio_data)
        except KeyboardInterrupt:
            # Handle KeyboardInterrupt immediately when it occurs
            raise
except KeyboardInterrupt:
    print("\nTerminating...")
    _stream.stop_stream()
    _stream.close()
    pa.terminate()

def process_audio_files():
for filename in [‘smoke1.mp3’, ‘smoke2.mp3’]:
print(f"Processing {filename}“) # Debugging output
audio = pydub.AudioSegment.from_mp3(filename)
audio = audio.set_frame_rate(SAMPLING_RATE) # Ensure correct sampling rate
audio = audio.set_channels(1) # Convert to mono if necessary
samples = np.array(audio.get_array_of_samples())
print(f"Number of samples: {len(samples)}”) # Debugging output
process_audio(samples)

if name == “main”:
# Start the keep_awake function in a separate thread
threading.Thread(target=keep_awake, daemon=True).start()

if PROCESS_AUDIO_FILES:
    process_audio_files()
else:
    process_live_audio()

Hi @Joe_L

Welcome to SmartThings Community

My first thought is that maybe the server on Glitch is sleeping, based on this:

if you reinstall the app you are forcing the server up, if I’m correct also trying to update the SmaryApp must solve the issue.

I believe the issue might be related to using the free version of Glitch. Even though I periodically ping the SmartApp from the Python app and get a successful response, the Glitch documentation mentions that servers on the free plan time out after 5 minutes. To prevent this, I ping it every 4 minutes:

def keep_awake():
while True:
try:
response = requests.get(‘link’)
if response.status_code == 200:
print(f’Successfully pinged your Glitch project at {GLITCH_PROJECT_URL}‘)
else:
print(f’Failed to ping your Glitch project. HTTP Status Code: {response.status_code}’)
except requests.RequestException as e:
print(f’An error occurred: {e}')

    for i in range(240):  # prints a message every second for 4 minutes
        print(f'Running... {i + 1}')
        sleep(1)
1 Like