Updated Dec 17, 2024

Bad Elves: Controlling Your Christmas Tree with Adalo, DreamFactory, Flask, and Duct Tape

Table of Contents
Text Link

ā€

ā€

Having spent my career writing software and often struggling mightily with the design, since joining the Adalo team a few months ago I've been pretty much blown away by how easy it is to create and wire up user interfaces using the Adalo app builder. Candidly I had always thought no-code app builders were limited to creating fairly simplistic CRUD-style (create, retrieve, update, delete) apps and dashboards. And while Adalo can definitely bring those types of ideas to life, I'm particularly fired up by the ability to integrate with third-party services like DreamFactory, Zapier and Integromat. You can also integrate (and even create!) third-party React components, opening up even more possibilities.

With Christmas fast approaching, and being the type of nerd who likes to automate things for no particular reason, a few days ago I started to wonder if it would be possible to create an Adalo app that could control my family Christmas tree lights. They're already wired up with a smart plug and our kitchen Alexa device and so can be turned on and off with the command, "Alexa, turn on the Christmas tree", but you know what's better than one home automation integration? Two. What follows is a recap of my quest to create and launch of a Christmas tree light management mobile app using Adalo, duct tape, and poor judgement.

The app will be available for a few days beginning Monday, December 16, and may be periodically offline due to me not being home (afraid of fire), my wife threatening to strangle me for making this public, or the obvious technical issues that may arise. Check it out at https://xmas.wjgilmore.com.

Introducing the App

The app is creatively called the Adalo XMASĀ Tree App. It consists of some theming, two buttons for toggling the Christmas lights, and a YouTube livestream so you can watch the action in realtime. But those buttons, oh those buttons are practically irresistible. You just can't help but press them and see that tree either light up in response or imagine my wife groaning after seeing it go dark yet again. Last night we were catching up on the Apple series Silo (season 2, watch it, it's amazing), and IĀ found myself in a death struggle with some unknown digital intruder. IĀ think I know who it was (Allison don't think IĀ don't know it was you!), but anyway the tree terrorist would click it off. And I would turn it back on. A few moments later the grinch was back and the lights were off again. And I would again valiantly fight back, illuminating the living room once again. They say heroes aren't born but forged in tinsel, and I'm living proof of it.

I'll bet your salivating at the idea of pushing those buttons. Well have at it. Go to https://xmas.wjgilmore.com and give them a press. WARNING:Ā IĀ have no idea how long this is going to be available before it's knocked offline by my ISP, a programming error, or frankly my wife.

My Christmas Tree App

Talking to the Smart Plug

Because in 2024 we can't be bothered with bending down to plug and unplug a string of lights, IĀ used a TP-Link KP115 smart plug to control the lights via Alexa. This is a pretty straightforward process, but to my knowledge there is no official way to interact with the plug outside of Alexa and the horrible manufacturer app. GitHub to the rescue! As expected, a kind programmer reverse engineered the plug communication protocol and bundled it up into a convenient Python package called python-kasa. To install python-kasa run:

$ pip install python-kasa

Once installed you can begin interrogating your local network of supported devices using the discover command:

$ python kasa discover
Discovering devices on 255.255.255.255 for 10 seconds
== Xmas tree - KP115(US) ==
Host: 192.168.86.42
Port: 9999
Device state: True
Time:         2024-12-12 11:36:41-05:00 (tz: EST)
Hardware:     1.0
Software:     1.0.21 Build 231129 Rel.171238
MAC (rssi):   00:5F:67:96:0A:7B (-38)

== Primary features ==
State (state): True
Current consumption (current_consumption): 252.6 W
Voltage (voltage): 122.0 V
Current (current): 2.08 A

== Information ==
On since (on_since): 2024-12-12 11:33:24-05:00
Today's consumption (consumption_today): 2.586 kWh
This month's consumption (consumption_this_month): 32.189 kWh
Total consumption since reboot (consumption_total): 13.381 kWh
Cloud connection (cloud_connection): True

== Configuration ==
LED (led): True

== Debug ==
RSSI (rssi): -38 dBm
Reboot (reboot): <Action>

Found 1 devices

So not only can you easily identify each device's assigned IPĀ address, you can also determine whether it's turned on, how long it has been in the on or off state, and how much power it has consumed both for the current month and since the last reboot. Pretty cool!

Once you know the device IPĀ address, it's easy to turn it on and off:

$ python kasa --host 192.168.86.42 on
Discovering device 192.168.86.42 for 10 seconds
Turning on Xmas tree

$ python kasa --host 192.168.86.42 off
Discovering device 192.168.86.42 for 10 seconds
Turning off Xmas tree

Creating an API

My ultimate goal was to control this plug via an Adalo mobile app which could be conceivably accessed from anywhere in the world including the North Pole. This means opening up an Internet connection from my home network to the outside world. Let me be clear: there are right ways to do this, and there are wrong ways. The way I'm about to show you is very, very wrong, and my home internet services provider will likely shut off our connection for doing it this way.

The simplest version of this APIĀ needs by my estimation three endpoints:Ā status, on, and off. The status endpoint tells us whether the plug is on or off, while the on and off endpoints are self-explanatory. IĀ used Flask to create the API, and found it to be very easy to use. To create an endpoint you just define the route URI and then the method that immediate follows is executed when that endpoint is requested. Believe it or not this is enough to create a functioning Flask API:

from flask import Flask

app = Flask(__name__)

@app.route("/status")
def status():
    return "hello"
    

Save this file as status.py or whatever and then start the Flask server like this:

$ flask --app status run
 * Serving Flask app 'hello'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Because Flask routes are by default GETĀ requests, you can just open your browser and go to http://127.0.0.1:5000/status to test it out. Presuming there are no syntax errors you should see the string hello displayed in the browser.

In the case of my project, I needed the APIĀ endpoints to execute python-kasa commands, and so I used Python's subprocess module to run shell commands. For the sake of this project IĀ can get away with playing things a bit fast and loose, however if you ever try something similar and need to pass parameters into the shell command then it is critically important that you validate the data before doing so. Here is the complete Flask script used for this project:

from flask import Flask
import subprocess, os, re

app = Flask(__name__)

@app.route("/status")
def status():
    output = subprocess.check_output(["python3", "kasa", "--host", "192.168.86.42", "state"],
    cwd='/Users/wjgilmore/Software/kasa/.venv/bin', text=True)

    pattern = r"Device state:\s*(True|False)"

    # Search for the pattern
    match = re.search(pattern, output)

    if match:
        device_state=match.group(1)
        return jsonify(
            state=device_state
        )
    else:
        return "Device state not found."

@app.post("/on")
def on():
    output = None
    try:
        output = subprocess.check_output(["python3", "kasa", "--host", "192.168.86.42", "on"],
        cwd='/Users/wjgilmore/Software/kasa/.venv/bin')
    except subprocess.CalledProcessError as e:
        return e.output
    return "on"

@app.post("/off")
def off():
    output = None
    try:
        output = subprocess.check_output(["python", "kasa", "--host", "192.168.86.42", "off"])
    except subprocess.CalledProcessError as e:
        return e.output
    return "off"

Talking to the Outside World

You probably noticed that the Flask API is running on localhost, meaning it's not accessible to the outside world. To make this APIĀ accessible elsewhere IĀ use a service called ngrok. ngrok has been around for as long as IĀ can remember, and long story short it will expose a local development server to the internet (among many other things). Using ngrok we can expose port 5000 of my local server to an ngrok.app subdomain as easy as running this command:

$ ngrok http --domain=myinternalapp.ngrok.app 5000
šŸ› Found a bug? Let us know: https://github.com/ngrok/ngrok
Sign  p to try new private endpoints ht ps://ngrok.com/new-features-update?ref=private
Session Status                online
Session Status                online
Update                        update available (version 3.18.4, Ctrl-U to update)
Version                       3.12.1
Region                        United States (us)
Latency                       95ms
Web Interface                 29ms //127.0.0.1:4040
Forwarding                    https://thisisnotmyendpoint.ngrok.app -> http://localhost:5000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              94      0       0.05    0.01    0.53    4.00
                              699     0         00      00       2    0 70
HTTP Requests
-------------

19:47:23.611 EST POST /off                      200 OK
14:42:48.366 EST GET  /status                   200 OK

Now IĀ can go to my ngrok endpoint from any network and it will forward the requests to and from my API!

Proxying the Requests Through DreamFactory

For both security and flexibility reasons IĀ then routed the ngrok endpoint through DreamFactory. DreamFactory is best known as a database wrapper capable of quickly exposing an enterprise-grade RESTĀ API, however it can do all sorts of other interesting things such as extending the capabilities of existing APIs. IĀ used DreamFactory's scripted service connector because IĀ thought it might be fun to eventually integrate other capabilities into the app such as the current temperature at the North Pole. Doing this is incredibly easy using DreamFactory because IĀ can just add new endpoints to my scripted service.

DreamFactory's scripted services connector supports the PHP, Python, and NodeJS languages, and because I'm most familiar with the platform's PHP-based APIĀ I went with that:

$api = $platform["api"];
$resource = $event['resource'];

if ($resource == "status") {

    $get = $api->get;
    $url = 'https://thisisnotmyendpoint.ngrok.app/status';
    $result = $get($url, json_decode($payload, true));
    
    return json_encode(["status" => $result['content']]);

} elseif ($resource == "on") {
    
    $post = $api->post;
    $url = 'https://thisisnotmyendpoint.ngrok.app/on';
    $result = $post($url, json_decode($payload, true));
    
    return json_encode(["status" => $result['content']]);

} elseif ($resource == "off") {
    
    $post = $api->post;
    $url = 'https://thisisnotmyendpoint.ngrok.app/off';
    $result = $post($url, json_decode($payload, true));
    
    return json_encode(["status" => $result['content']]);
    
} elseif ($resource == "temperature") {

    return json_encode(["status" => "Freezing"]);

}

Just for sake of illustration I've extended what's exposed through the Flask API by adding a /temperature endpoint which when requested will return "Freezing".

After saving these changes I added a role-based access control to the DreamFactory API and then generated an API key. The DreamFactory API endpoints are now only accessible by providing the APIĀ key which is provided via a secure HTTPĀ header.

Integrating the APIĀ and Adalo App

One of my favorite Adalo builder features is how easy it is to tie events to user actions. For instance when clicking on the "Turn tree on"Ā button an API call to the /on endpoint needs to happen. This is accomplished as easily as adding an "action" to the button and indicating what kind of event needs to occur in order for that action to execute. Further, you can define multiple actions as shown in this screenshot (taken from the actual app):

ā€

Tying events to button clicks

The APIĀ call is sent to the aforementioned DreamFactory endpoint. Again, the call is secured by passing along an APIĀ key via the HTTPĀ header. Defining this call is accomplished via a simple web wizard, one step of which is shown in the following screenshot:

Defining an APIĀ call

Counting Light Toggle Frequency

I thought it would be fun to keep track of the number of times users have turned lights on and off. IĀ could do this within the local Flask API using SQLite, but thought it made sense to keep this tally closer to the app and so used Adalo's Collections feature instead. IĀ created a single collection called counts consisting of four fields: ID (the usual auto-incrementing primary key), name (which stores the strings on and off , and the usual timestamps. Usual caveats apply here, IĀ could have probably set this up a bit more efficiently but we're just bulldozing our way through the project. Here is an example of a few records stored in the table:

Tracking the times lights have turned on and off

ā€

Charting the Results

My friend Nic over at DreamFactory suggested creating a chart depicting the toggling frequency. IĀ didn't feel like writing any custom code to implement this however it's trivial to export data out of an Adalo collection and so IĀ did that and uploaded it along with an example chart into ChatGPT:

Generating a chart with ChatGPT

Yes I am always polite when talking to AI these days. I for one welcome and appreciate our robot overlords. IĀ didn't like how the chart turned out though and so after a few more attempts IĀ asked it to create a pie chart instead:

Looks like we have a lot of elf sympathizers

ā€

Setting Up the YouTube Livestream

Setting up the YouTube livestream was really straightforward; IĀ used Adalo's YouTube marketplace component, pasted in the livestream URL, and it just worked. Plus IĀ get to watch the livestream status using this fun YouTube Studio interface:

ā€

Creating the App Icon

I have no design skill whatsoever, and so just relied on DALL-E to generate one for me. I used the prompt "Please create a 1024 x 1024 px icon for a mobile app used to Control Christmas tree lights". DALL-E ignored the dimensions and created the two options found in the screenshot:

Using DALL-E to generate an app icon
Using DALL-E to generate an app icon

In the interests of time, IĀ selected the very first icon it created, downloaded it to my Mac, and then used macOS' little-known remove background feature to remove the background gradient:

Removing an image background with macOS

Conclusion

This was a very fun project that made my kids and neighbors laugh, and aggravated my wife to no end. Sometimes the best projects are those which have no real purpose other than to have a few laughs. Hopefully by the time you read this my house hasn't burned down. Perhaps in a later post I'll document some of the other funny things which popped up during development, such as Treehouse instructor Laura Coronel writing an automated script to interact with the user interface and toggle the tree lights 100 times in one minute.

Start Building With An App Template
Build your app fast with one of our pre-made app templates
Try it now
Read This Next

Looking For More?

Ready to Get Started on Adalo?