Light Show On-Demand via SMS/Text
Posted on November 23, 2025 • 9 min read • 1,904 words
Background
This solution pre-dates the web based solution by a few years. I wanted a quick and easy way for my show visitors to trigger my light show so that it only ran the flashy lights when someone was actually there to watch, and reduce disturbing my neighbors.
At the time, there were a few FPP plugins available that might have done the job, but since I was already using home automation tools to do more than just control my light show, and I wanted more control to do some things I’ll describe below, I did it myself.
This guide walks through the features, key concepts and some detailed setup steps.
The Goal
The objective is allow visitors to interact with the light show via SMS/text message. We want to publish a phone number to which they can text any number of commands to trigger various light show functions, including playing the entire show, playing a single sequence, retrieving light show information, donation information, or anything else you can think of. The feature should also respond to visitors by text to acknowledge receipt, provide status or other requested information, or let them know if there was an error.
Prerequisites
- FPP: Running your holiday sequences.
- SMS/Text Provider: I recommend VoIP.ms , and that’s what I will be referencing in this guide. You need a service that takes an incoming text message and relays that to a callback URL/API endpoint/webhook.
- Command Broker: I recommend something like Node-RED , but if you already use HomeAssistant , much of this can be done with HomeAssistant Automations too. If you don’t want to use a command broker and instead have FPP do all the work, you’re probably better off using one of the existing plug-ins.
The Workflow
- The Text Message: Light show visitor sends a text message to the published phone number (hosted at VoIP.ms) with a command.
- Forward the Payload: VoIP.ms forwards the originating phone number and command text to a callback URL (a HomeAssistant webhook, a Node-RED HTTP IN node, a Cloudflare Worker acting as a secure proxy, etc.)
- Process the Payload: The Command Processor evaluates the message to extract the command and routes to the appropriate flow to control the show.
- Respond to Message: Send a message back to the show visitor with status or information requested, based on the command.
Implementation
The Gateway: VoIP.ms Configuration
I won’t get into all the details here. But once you sign up and have your DID number, assuming you don’t want to receive phone calls here, is to navigate to DID Numbers | Manage DIDs. Below, I’ve detailed the settings you should choose, and skipped those I think are irrelevant.
- Routing Settings: Check System and select Hangup.
- DID Point of Presence: If not automatically selected, choose one closest to you.
- Message Service (SMS/MMS):
- Message Service (SMS/MMS): Enabled
- SMS/MMS Forward to an Email Address: (Optional) Enter your email address if you want incoming text messages emailed to you.
- SMS/MMS Forward to a Phone Number: (Optional) Enter a number here if you want incoming messages forwarded to another number.
- SMS/MMS URL Callback: (Important)
https://[YOUR_IP_ADDRESS_OR_DOMAIN]/[YOUR_WEBHOOK_OR_ENDPOINT]?from={FROM}&message={MESSAGE}
The Command Broker: Receiving and Routing
Node-RED handles parsing the inbound text message payload and figuring out what to do with it. As a first step, you just need to get that message data into Node-RED.
- HTTP IN (node): This node receives the inbound payload from VoIP.ms, stored in
msg.payload.- Method: GET
- URL: Set this to the same SMS/MMS URL Callback URL from your VoIP.ms configuration above.
- Output: Connects to the HTTP Response and Switch nodes.
- HTTP Response (node): Be nice and send a response back to VoIP.ms.
- Input: Connects from the HTTP In node.
- Status Code: 200
- Switch (node): Handles the initial routing based on the received command, in
msg.payload.message.- Input: Connects from the HTTP In node.
- Property:
$lowercase(payload.message)Using lowercase() for comparison. - Output (rules): This will vary based on what commands you want to receive and act on. Here are some examples:
| Output | Rule Type | Command Match | Destination |
|---|---|---|---|
| Output 1 | Equals | info |
Respond with instructions & link to website. |
| Output 2 | Equals | donate |
Respond with info & link to my favorite charity. |
| Output 3 | RegEx | ^go(\d+)?$ |
The main play/error path. Matches anything starting with go. |
| Output 4 | Equals | bday |
Plays a special birthday sequence for a friend & overrides ‘checks’. |
| Output 5 | Otherwise | (Default) | Respond with, “I didn’t understand, reply with info for help.” |
Validating the Request: The ‘Checks’
If the incoming command matches the “go” regex (ie, is go alone or followed by a number) and is routed out the main play/error path, the system performs a series of prerequisite checks before fulfilling the request. These will vary based on what is important to you. Here are some samplee cases to test for:
- Is it ‘Show Season’?: A Switch node confirms a ‘holiday’ variable is set. If it fails, the system replies with a message like, “We’re done for the season. Check back in September.” or “We’re changing over to Christmas. Check the web for start dates.” I change this often.
- Daylight: A within-time node checks to ensure it’s dark outside. If the message is received between sunrise and sunset, the system replies with a message asking the visitor to come back after dark.
- Is On-Demand On?: My lights are on from sunset until midnight, but I don’t want sequences triggered late at night (eg, after 9:30pm on school nights). I also only want to display my phone number when the service is available. I accomplish this by using a toggle switch in HomeAssistant that I can automatically or manually control. This check confirms that the on-demand service is currently enabled. If it fails, the system replies with a messaging like, ‘On-Demand is off for maintenance or outside show hours. Visit again soon.’
- Is FPP Playing?: I don’t want a new request to interrupt if another sequence is already playing. This check confirms the status before moving on. It checks the MQTT topic
fpp01/falcon/player/FPP/statusto confirm the player is idle. If it fails, the system replies saying, “Sorry. Something else is playing now. Try again when it finishes.” - Presence Detection (testing): Sometimes the show is triggered when no one is watching. I suspect it’s just neighborhood kids playing around. I’ve started testing object detection from my security cameras to confirm there is a car parked or a person standing in front of the house before proceeding to play.
Action and Integration
If the received command passes all the checks, it’s time to play something. My show plays in three modes:
-
Randomized Playlist: When my show ‘starts up’ at sunset every night, it checks the date and then populates a HomeAssistant
input_select.fpp_sequencesentity from the holiday-specific playlists set up in FPP. If the command is go (or go0, but no one does this), it checks aninput_select.song_playlistentity for length (this will make sense in a minute).If the length is <= 1, Node-RED randomizes the items from
input_select.fpp_sequences, populates theinput_select.song_playlistentity with the full randomized list, then plays from that list. As my Halloween and Christmas shows have 30+ sequences each and few people will sit for hours to watch all of them in one session, I play a set of 4-6 sequences at a time. When it’s done, if they want more, the visitor can text go again to play the next set, picking up where they left off. Since the playlist has more than one entry, it will not re-randomize on subsequent triggers. -
Specific, Selected Sequence: If the command is goN (where N is a non-zero number), Node-RED performs a lookup against
input_select.fpp_sequences, gets the title of the Nth sequence, puts it ininput_select.song_playlistand plays it. -
Single, Randomized Sequence (on a timer): Not directly related to the on-demand feature, I also have a timer running to play a random sequence every 15 minutes. When this executes, it runs the same randomization flow as the Randomized Playlist above, but populates
input_select.song_playlist, with only the first song. The idea here is that if a visitor triggers 1 or 2 sets then leaves, this timed event will fire about 15 minutes after the last sequence was played.
In the two latter scenarios, since input_select.song_playlist has only one entry, the next visitor to trigger the Randomized Playlist flow with go will get a new, randomized playlist.
Playing a sequence from Node-RED is pretty straightfoward.
- Use a HomeAssistant Current State node to get the selected sequence title from the target input_select entity into
msg.payload. - Then, use a Template node to build the FPP command to be published to MQTT (eg,
{"command":"Start Playlist","args":["{{payload}}.fseq",false,false]}). - Finally, use an MQTT Out node to push that to the
fpp01/falcon/player/FPP/set/commandtopic.
Preparing and Sending the Response
I have about 10 total possible SMS replies to all of the above various situations. There are numerous ways one could go about managing them, including hard-coding into Change nodes or consolidating into a single Function node (which I’m considering). For now, though, I use a HomeAssistant input_text helper entity for each response message that I call with a HomeAssistant Current State node.
Whether it’s a reply in response to an information request, a failure on one of the prerequisite checks, or letting the visitor know “The show is starting”, I pull in the appropriate message and save it to msg.data. I use a HomeAssistant Action node to call rest_command.sms_reply service because it’s already set up for other uses. But as you will see, you could simply send the message with an HTTP Request node.
Node-RED Flow
- Current State (node): Gets the message text from the desired input_text entity.
- Input: Connects from virtually any node near the end of the flow that contains the original
msg.payload. - Entity ID: The entity_id of the input_text entity (eg,
input_text.off_season). - Output Properties:
msg.data=entity state. - Output: Connects to the Change node.
- Input: Connects from virtually any node near the end of the flow that contains the original
- Change (node): This is optional, but saves time. I append a link to my website at the end of every response.
_ Input: Connects from the Current State node.
- Rules:
- Set |
msg.data - to the value
data & " Visit https://lightsongranger.com for more info."(JSONata)
- Set |
- Output: Connects to the Action node.
- Rules:
- Action (node): - This calls the
rest_commandservice to send the message via VoIP.ms.- Input: Connects from the Change node.
- Server: Assuming, by now, you have this set up already, select your HomeAssistant instance.
- Action:
rest_command.sms_reply(or whatever you’ve named your service, see below). - Data: Select JSONata Expression and enter
{"dst":"{{payload.from}}","message":"{{{data}}}"}
HomeAssistant Rest Command Entity
To send the SMS message, use the VoIP.ms API to send a GET request like this:
https://voip.ms/api/v1/rest.php?api_username=[YOUR_VOIPMS_USERNAME]&api_password=[YOUR_VOIPMS_PASSWORD]&method=sendSMS&did=[YOUR_VOIPMS_PHONE_NUMBER]&dst={{ dst }}&message={{ message }}"If using HomeAssistant, add this to your configuration.yaml under your rest_command section:
sms_reply:
url: "https://voip.ms/api/v1/rest.php?api_username=[YOUR_VOIPMS_USERNAME]&api_password=[YOUR_VOIPMS_PASSWORD]&method=sendSMS&did=[YOUR_VOIPMS_PHONE_NUMBER]&dst={{ dst }}&message={{ message }}"
method: getConclusion
Even though I recently added the ability to trigger the show from a web page, thus far I still have many more visitors using this SMS/text method. Few seem that interested in selecting specific songs and instead prefer triggering a set from the playlist, then sitting back to watch whatever plays.
But whichever method, I do think having an on-demand option to limit the playing of flashy sequences when no one is watching goes a long way to appease any neighbors who might be slightly (or more) annoyed with the show and traffic.
I hope this has been helpful and inspires you to make your own.