The Idea
Recently, my kids were asking about getting an Elf on the Shelf. But my wife and I are not keen to said elf, so I thought up an alternative solution. There is too much pressure to do something cute and original with the Elf every morning and I don't have the ambition for any of that. So instead, my kids can check and see exactly how good or bad they are directly from Santa's naughty and nice server.
I had a mini servo lying in my parts bin, that I have yet to find a use for, so I thought an analog meter would be a nice and easy visual for my children to read. I used my trusty ESP8266 and began modifying some home automation code that I had to work with the servo and strip out everything that else. The device communicates via MQTT and push buttons are used to switch between child.
Implementation
I first started with the hardware and coding the bare ESP-12E on my breadboard for programming these devices. The ESP8266 is a great microcontroller. It runs on 3.3 volts, has built in WiFi, and costs less than $2 for these bare boards from China. I've been using these in a lot of Internet of Things (IOT) and home automation creations.
The software is written directly to the ESP board using Arduino software and my most current version of the code below. It uses many libraries to make configuring of the device fairly easy to the user. There are two buttons on my device and these are used to switch from one child to another. I also used these buttons in order to reset the device as well as put it into Access Point Setup.
In setup mode, the device acts as an access point. When you connect, it presents a captive portal that takes you to the web page to setup the wifi for connection. I also added additional fields to configure the MQTT server address, username, password, and topic to subscribe to. The MQTT topic uses JSON data to clearly and quickly use that information for different systems.
#define firmware_version "esp_mqtt_naughtynice-0.64" /* -----| ESP-12e |------- * * v0.6 - Bug fixes to dimming * v0.5 - Added LED dimmer * v0.4 - Changed interrupt pins * v0.3 - Fixed external reset * v0.2 - Updates and bug fixes * v0.1 - Initial version * * GPIO15 - to GND (through 10K ohm) at boot * GPIO2 - to +3.3 (through 10K ohm) at boot * GPIO0 - to +3.3 (through 10K ohm) to Run |OR| to GND to Program * CH_PD - to +3.3 (through 10K ohm) * RESET - to +3.3 (through 10K ohm) * GPIO16 - to RESET for deepSleep timer * * +---------------+ * | |-| |-| |-- | * | | |_| |_| | * +--------- RESET O |-------- O IO 1 - TX * | ADC O +---------+ O IO 3 - RX * | [3.3v] - CH_PD O | | O IO 5 - SCL [LED 2] * +--SLEEP - IO 16 O | ESP | O IO 4 - SDA [LED 1] * [Button 2] IO 14 O | 12e/f | O IO 0 [3.3v] * [Servo] IO 12 O | | O IO 2 [3.3v] * [Button 1] IO 13 O | | O IO 15 [GND] * VCC O +---------+ O GND * +--O-O-O-O-O-O--+ * \ \ \ \ \ \--IO 6 - SPI2_CLK * \ \ \ \ \--IO 8 - SPI2_MOSI * \ \ \ \--IO 10 * \ \ \--IO 9 - SPI2_MISO * \ \--IO 7 - SPI2_CS0 * \--IO 11 * */ #include <FS.h> //this needs to be first, or it all crashes and burns... #include <ESP8266WiFi.h> #include <ESP8266WiFiMulti.h> #include <DNSServer.h> //Local DNS Server used for redirecting all requests to the configuration portal #include <ESP8266WebServer.h> //Local WebServer used to serve the configuration portal #include <WiFiManager.h> #include <ESP8266HTTPClient.h> #include <ESP8266httpUpdate.h> #include <ArduinoJson.h> #include <PubSubClient.h> #include <Servo.h> Servo meter; WiFiClient espClient; PubSubClient client(espClient); //update server information #define update_server "jonmayer.duckdns.org" #define update_port 8080 #define update_uri "/esp8266/update.php" //adjustable constants #define FORMAT_SPIFFS false //pin definitions //#define TRIGGER_PIN 13 //for reset and wifi setup #define SERVO_PIN 12 #define JACOB_BTN 13 #define ALEXANDER_BTN 14 #define JACOB_LED 4 #define ALEXANDER_LED 5 //LEDs sink directly from GPIO @ 20mA #define ON 0 #define OFF 1023 #define DIM_TIME 5 #define DIM_LVL 850 //wifi setup mqtt variables char mqtt_server[40]; char mqtt_port[6]; char mqtt_user[25]; char mqtt_pass[25]; char mqtt_topic_santa[33]; //light variables bool lightState = false; int lightBright = 255; bool isRGB = false; int lightR = 0; String currentChild = "Jacob"; int jacob = 0; int alexander = 0; int dimmerTimer = 0; int resetTimer = 0; bool resetting = false; //flag for saving data bool shouldSaveConfig = true; //callback notifying us of the need to save config void saveConfigCallback () { Serial.println(F("Should save config")); shouldSaveConfig = true; } void iotUpdater(bool debug) { Serial.println("checking for update..."); if (debug) { //printMacAddress(); Serial.print(update_server); Serial.print(update_port); Serial.println(update_uri); Serial.println(firmware_version); } t_httpUpdate_return ret = ESPhttpUpdate.update(update_server, update_port, update_uri, firmware_version); switch (ret) { case HTTP_UPDATE_FAILED: if (debug) Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); break; case HTTP_UPDATE_NO_UPDATES: if (debug) Serial.println(F("HTTP_UPDATE_NO_UPDATES")); break; case HTTP_UPDATE_OK: if (debug) Serial.println(F("HTTP_UPDATE_OK")); break; } } void saveConfig(){ Serial.println(F("saving config")); DynamicJsonBuffer jsonBuffer; JsonObject& json = jsonBuffer.createObject(); json["mqtt_server"] = mqtt_server; json["mqtt_port"] = mqtt_port; json["mqtt_user"] = mqtt_user; json["mqtt_pass"] = mqtt_pass; json["mqtt_topic_santa"] = mqtt_topic_santa; File configFile = SPIFFS.open("/config.json", "w"); if (!configFile) { Serial.println("failed to open config file for writing"); } json.prettyPrintTo(Serial); json.printTo(configFile); configFile.close(); //end save } void readConfig(bool formatSpiffs){ //clean FS, for testing if(formatSpiffs) SPIFFS.format(); //read configuration from FS json Serial.println(F("mounting FS...")); if (SPIFFS.begin()) { Serial.println(F("mounted file system")); if (SPIFFS.exists("/config.json")) { //file exists, reading and loading Serial.println(F("reading config file")); File configFile = SPIFFS.open("/config.json", "r"); if (configFile) { Serial.println(F("opened config file")); size_t size = configFile.size(); // Allocate a buffer to store contents of the file. std::unique_ptr<char[]> buf(new char[size]); configFile.readBytes(buf.get(), size); DynamicJsonBuffer jsonBuffer; JsonObject& json = jsonBuffer.parseObject(buf.get()); json.printTo(Serial); if (json.success()) { Serial.println(F("\nparsed json")); strcpy(mqtt_server, json["mqtt_server"]); strcpy(mqtt_port, json["mqtt_port"]); strcpy(mqtt_user, json["mqtt_user"]); strcpy(mqtt_pass, json["mqtt_pass"]); strcpy(mqtt_topic_santa, json["mqtt_topic_santa"]); } else { Serial.println(F("failed to load json config")); } } } } else { Serial.println(F("failed to mount FS")); } //end read } void jsonEncodeData(){ StaticJsonBuffer<100> jsonBuffer; JsonObject& json = jsonBuffer.createObject(); json["jacob"] = jacob; json["alexander"] = alexander; //json.prettyPrintTo(Serial); char serialized[100]; json.printTo(serialized, 100); client.publish(mqtt_topic_santa, serialized, true); } void jsonDecodeData(char* json, bool callback){ bool jacobChange = false; bool alexanderChange = false; StaticJsonBuffer<200> jsonBuffer; JsonObject& jsonOut = jsonBuffer.parseObject(json); if (jsonOut.success()) { Serial.println(F("Received JSON Data:")); jsonOut.prettyPrintTo(Serial); if (jsonOut.containsKey("reset")){ resetTrigger(); } if (jsonOut.containsKey("jacob")){ if((int) jsonOut["jacob"] != jacob){ jacobChange = true; jacob = (int) jsonOut["jacob"]; } } if (jsonOut.containsKey("alexander")){ if((int) jsonOut["alexander"] != alexander){ alexanderChange = true; alexander = (int) jsonOut["alexander"]; } } if (alexanderChange && !jacobChange){ if(currentChild == "Jacob") changeAlexander(); } if (jacobChange && !alexanderChange){ if(currentChild == "Alexander") changeJacob(); } if ((jacobChange || alexanderChange) && callback){ dimmerTimer = 0; jsonEncodeData(); }else{ client.publish("esp/init", "OK", false); } }else{ Serial.println(F("JSON parsing errror")); } } void mqttCallback(char* topic, uint8_t* payload, unsigned int length) { char* value = (char *)payload; value[length] = '\0'; //add null character after length of data to terminate if(strcmp(topic,mqtt_topic_santa)==0){ //for loading initial values from MQTT jsonDecodeData(value, false); client.unsubscribe(mqtt_topic_santa); }else{ jsonDecodeData(value, true); } } void MQTT_connect() { // Loop until we're reconnected while (!client.connected()) { Serial.print(F("Attempting MQTT connection...")); // Create a random client ID String clientId = "ESP8266Client-"; clientId += String(random(0xffff), HEX); // Attempt to connect if (client.connect(clientId.c_str(),mqtt_user,mqtt_pass)) { Serial.println(F("MQTT Client Connected")); char mqtt_topic_set[32+5]; char topic_set[5] = "/set"; strcat(mqtt_topic_set, mqtt_topic_santa); strcat(mqtt_topic_set, topic_set); client.subscribe(mqtt_topic_set); // home/test/jsonLight/set client.subscribe(mqtt_topic_santa); //Get last values after reset Serial.print(F("Subscribed to topic: ")); Serial.println(mqtt_topic_set); } else { Serial.print(F("failed, rc=")); Serial.print(client.state()); Serial.println(F(" try again in 3 seconds")); // Wait 3 seconds before retrying delay(3000); } } } void doubleFlash(){ analogWrite(JACOB_LED, OFF); analogWrite(ALEXANDER_LED, OFF); delay(10); analogWrite(JACOB_LED, ON); analogWrite(ALEXANDER_LED, ON); delay(200); analogWrite(JACOB_LED, OFF); analogWrite(ALEXANDER_LED, OFF); delay(100); analogWrite(JACOB_LED, ON); analogWrite(ALEXANDER_LED, ON); delay(200); analogWrite(JACOB_LED, OFF); analogWrite(ALEXANDER_LED, OFF); } void resetTrigger(){ resetting = true; doubleFlash(); ESP.restart(); yield(); delay(500); } void changeJacob(){ dimmerTimer = 0; currentChild = "Jacob"; analogWrite(JACOB_LED, ON); } void changeAlexander(){ dimmerTimer = 0; currentChild = "Alexander"; analogWrite(ALEXANDER_LED, ON); } int getPosition(int childValue){ return map(childValue,0,100,0,167); } unsigned long timeNow = 0; unsigned long timeLast = 0; void setup() { Serial.begin(115200); Serial.println(F("booting...")); // is configuration portal requested? pinMode(JACOB_BTN, INPUT); pinMode(ALEXANDER_BTN, INPUT); pinMode(JACOB_LED, OUTPUT); pinMode(ALEXANDER_LED, OUTPUT); digitalWrite(JACOB_LED, OFF); digitalWrite(ALEXANDER_LED, OFF); //readConfig(false); readConfig(FORMAT_SPIFFS); //to format SPIFFS (first run) delay(500); int resetVal = digitalRead(JACOB_BTN) + digitalRead(ALEXANDER_BTN); Serial.println(resetVal); //attachInterrupt(TRIGGER_PIN, resetTrigger, FALLING); if ( resetVal == 0 ) { doubleFlash(); Serial.println(F("Entering AP mode for configuration")); WiFiManager wifiManager; wifiManager.setSaveConfigCallback(saveConfigCallback); wifiManager.setConfigPortalTimeout(180); // 3 minutes WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 39); WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 5); WiFiManagerParameter custom_mqtt_user("username", "mqtt username", mqtt_user, 24); WiFiManagerParameter custom_mqtt_pass("password", "mqtt password", mqtt_pass, 24); WiFiManagerParameter custom_mqtt_topic_santa("santa_topic", "mqtt santa topic", mqtt_topic_santa, 32); wifiManager.addParameter(&custom_mqtt_server); wifiManager.addParameter(&custom_mqtt_port); wifiManager.addParameter(&custom_mqtt_user); wifiManager.addParameter(&custom_mqtt_pass); wifiManager.addParameter(&custom_mqtt_topic_santa); wifiManager.startConfigPortal("OnDemandAP"); Serial.println(F("connected!")); strcpy(mqtt_server, custom_mqtt_server.getValue()); strcpy(mqtt_port, custom_mqtt_port.getValue()); strcpy(mqtt_user, custom_mqtt_user.getValue()); strcpy(mqtt_pass, custom_mqtt_pass.getValue()); strcpy(mqtt_topic_santa, custom_mqtt_topic_santa.getValue()); if (shouldSaveConfig) { saveConfig(); delay(500); } }else{ Serial.println(F("Trying to autoConnect() to Wifi")); WiFiManager wifiManager; wifiManager.setConfigPortalTimeout(3); //3 second portal timeout on no connection if(!wifiManager.autoConnect()) {//is needed? Serial.println(F("Failed to connect to wifi, shutting down")); // deepSleep time is defined in microseconds. Multiply // seconds by 1e6 - 30000000 = 30 sec ESP.deepSleep(30000000); //reboot after 30 seconds (GPIO16 connected to RST) delay(100); } } iotUpdater(false); //auto update function (false for no debug report) // Setup MQTT subscription for onoff feed. Serial.print(F("Connecting to: ")); Serial.println(mqtt_server); Serial.print(F("On port: ")); Serial.println(mqtt_port); unsigned int port = atoi(mqtt_port); Serial.println(port); client.setServer(mqtt_server, port); client.setCallback(mqttCallback); MQTT_connect(); meter.attach(SERVO_PIN); attachInterrupt(JACOB_BTN, changeJacob, FALLING); attachInterrupt(ALEXANDER_BTN, changeAlexander, FALLING); } void loop() { if(!resetting){ timeNow = millis()/1000; int seconds = timeNow - timeLast; // put your main code here, to run repeatedly: if (!client.connected()) { MQTT_connect(); } client.loop(); int resetVal = digitalRead(JACOB_BTN) + digitalRead(ALEXANDER_BTN); if (resetVal == 0){ if(resetTimer == 0) resetTimer = seconds; else if((seconds - resetTimer) > 3){ resetTrigger(); } }else{ resetTimer = 0; } if(dimmerTimer != 0 && (seconds - dimmerTimer) >= DIM_TIME){ if(currentChild == "Jacob"){ analogWrite(JACOB_LED, DIM_LVL); analogWrite(ALEXANDER_LED, OFF); } if(currentChild == "Alexander"){ analogWrite(ALEXANDER_LED, DIM_LVL); analogWrite(JACOB_LED, OFF); } }else{ if(currentChild == "Jacob"){ meter.write(getPosition(jacob)); // tell servo to go to position in variable 'pos' analogWrite(JACOB_LED, ON); analogWrite(ALEXANDER_LED, OFF); } if(currentChild == "Alexander"){ meter.write(getPosition(alexander)); analogWrite(ALEXANDER_LED, ON); analogWrite(JACOB_LED, OFF); } if(dimmerTimer == 0) dimmerTimer = seconds; delay(15); } } }
Obviously, there are a lot of hard coded values for both of my kids. But one could easily modify this for different children and/or add or remove kids as well. The most simple configuration would be using the meter for one child. In this configuration, you wouldn't need a button unless you wanted to keep the ability to reset and to put into access point set
bviously, there are a lot of hard coded values for both of my kids. But one could easily modify this for different children and/or add or remove kids as well. The most simple configuration would be using the meter for one child. In this configuration, you wouldn't need a button unless you wanted to keep the ability to reset and to put into access point setup.The data can be simply sent to your MQTT topic via the set path and the device will do everything else. For Example: if you use the topic "meter/santa" then to set a new value, you would send JSON data to "meter/santa/set" without the retain flag. The ESP will decode the JSON, update the values, publish the new values to "meter/santa" and then update the servo position. This all happens within a second to publishing the first set topic. The JSON is simple and in my case looks something like this: {"Jacob":0,"Alexander":0}. The numeric value is 0 to 100% and represents "percent naughty". 0% points all the way to the right, while 100% points all the way to the left.
Getting Fancy
But I didn't want to have to set the values every time my kids did something bad (or good). After manually testing and making sure the data was received correctly and updating as it should, I decided to automate this update with SMS. My wife had the idea of texting Santa from a Facebook post about renaming a contact in your phone to Santa to keep tabs on your kids. I liked the idea, but didn't like having to depend on someone else to answer, so I looked into APIs for Google Hangouts and Google Voice Messaging.
While some services are available, most are outdated for current Google APIs. I decided JavaScript would be much easier to use for getting and receiving SMS with Google Voice. It's really rough and probably won't work if you used your Google Voice for a lot of SMS, but I happened to have an account that I don't really use for anything but voicemail. My code for the chrome extension is below, but you can just as easily copy and paste this into the developer console as well. I made it an extension so I wouldn't have to worry if the page reloaded.
var childNames = { "Jacob":["jacob","jake"], "Alexander": ["alexander","xander"] }; var bothNice = [ "That is wonderful news and what I like to hear.", "Keep up the good work boys. Christmas is closer than you think.", "Those two are making my job so much easier when they are nice.", "Mrs Clause told me they were being good little boys too.", "Ho Ho Ho! That is terrific!" ]; var bothNaughty = [ "They better be careful or they might find themselves on the naughty list.", "I've never brought those boys coal, but maybe this year.", "They don't want to be on the naughty list, do they?", "I see them when they're sleeping and know when they are bad", "I might have to just give their presents to their parents." ]; var singleNice = [ "I think {{child}} knows how to get on the nice list.", "Keep it up {{child}}! It's easier to be nice than naughty.", "If {{child}} keeps being good, I might have to bring an extra gift.", "We wish you a Merry Christmas, and a happy {{child}}.", "I'll make sure to fill his stocking extra big, if {{child}} can keep being this good." ]; var singleNaughty = [ "Tell {{child}} that he needs to behave better or else I'll have to put him on the naughty list.", "Jingle bells, naughty boys smell. Don't be a rotten egg {{child}}.", "It makes me so sad to here that {{child}} is misbehaving so close to Christmas", "That is disappointing. {{child}} needs to work harder.", "{{child}} needs to listen and help out more, or it's the naughty list for him." ]; var niceKeys = [ "good", "well behaved", "nice", "happy", "helpful", "kind", "patient", "awesome", "great", "amazing", "wonderful", "good behavior", "sweet", "good listener", "being a better listener" ]; var naughtyKeys = [ "bad", "not listening", "work on listening", "needs to listen", "naughty", "crabby", "whiney", "in trouble", "having trouble", "didn't clean", "didn't help", "won't help", "not behaved", "bad behavior", "rude", "brat", "punk", "impatient", "attitude" ]; var kidData; var santaData; var sendData = true; function updateData(){ $.ajax({ method: "POST", url:"https://192.168.1.93:443/mqtt/subscribe.php", data: {"topics":["home/naughtynice"]} }).done(function(resp){ kidData = resp.data[0].message; if(sendData) publishUpdate(); }).fail(function(resp){ console.warn(resp); }); } function publishUpdate(){ var tempObj = {}; Object.keys(kidData).forEach(function(key) { santaData["child"].forEach( child => { if(child=="Both" || key==child.toLowerCase()){ tempObj[key] = kidData[key] + santaData["change"]; } }); }); $.ajax({ method: "POST", url:"https://192.168.1.93:443/mqtt/publish.php", data: {"topics":["home/naughtynice/set"], "message":tempObj} }).done(function(resp){ console.log(resp); }).fail(function(resp){ console.warn(resp); }); } function Random(fromNum,toNum){ return Math.floor(Math.random()*toNum)+fromNum; } function checkNaughtyNiceLevel(child, niceCounter, naughtyCounter){ santaData = {"child": child, "change": naughtyCounter - niceCounter}; updateData(); } var loading = false; var refresher = window.setInterval(function(){ var contacts = document.querySelectorAll(".md-virtual-repeat-offsetter > div"); for(var i = 0; i < contacts.length; i++){ var contact = contacts[i].innerText.split('\n')[0]; var selectableElement = contacts[i].children[0].children[0].children[0].children[0]; var dummyElement = contacts[contacts.length-1].children[0].children[0].children[0].children[0]; if(!loading && selectableElement.getAttribute("aria-label").indexOf("Unread")==0){ console.log("unread message incoming"); if(selectableElement.getAttribute("aria-selected") != "true"){ fire(selectableElement,"click"); window.setTimeout(function(){ fire(dummyElement,"click"); }, 1000); } window.setTimeout(function(){ fire(selectableElement,"click"); window.setTimeout(getMessage, 3000); //wait 3 seconds to max sure it loads },3000); loading = true; return; } //} } }, 3000); function sendMessage(text){ var inputBox = document.getElementsByTagName("textarea")[0]; var inputButton = document.getElementsByTagName("button"); inputButton = inputButton[inputButton.length-1]; inputBox.value = text; fire(inputBox, "change"); fire(inputButton, "click"); } function parseMessage(text){ text = text.toLowerCase(); var children = []; var msgToSend; Object.keys(childNames).forEach(function(key) { childNames[key].forEach( alias => { //console.log(alias); if(text.indexOf(alias)!=-1){ children.push(key); } }); }); if(children.length==1){ child = children[0]; }else{ child = "Both"; } if(typeof(child) !== "undefined" && child != ""){ if(child == "Both") sendMessage("Thank you for the update on Jacob and Alexander."); else sendMessage("Thank you for the update on " + child + "."); var naughtyCount = 0; var niceCount = 0; for(var i=0; i<naughtyKeys.length; i++){ if(text.indexOf(naughtyKeys[i])!=-1) naughtyCount+=15; } for(var i=0; i<niceKeys.length; i++){ if(text.indexOf(niceKeys[i])!=-1) niceCount+=15; } if(naughtyCount > niceCount){ if(child == "Both") msgToSend = bothNaughty[Random(0,bothNaughty.length)]; else msgToSend = singleNaughty[Random(0,singleNaughty.length)].replace("{{child}}",child); }else if(niceCount > naughtyCount){ if(child == "Both") msgToSend = bothNice[Random(0,bothNice.length)]; else msgToSend = singleNice[Random(0,singleNice.length)].replace("{{child}}",child); }else{ return; } window.setTimeout(function(){ sendMessage(msgToSend); }, Math.floor(Math.random()*6000)+1000); checkNaughtyNiceLevel(children, niceCount, naughtyCount); } if(text.indexOf("no ")==0 || text.indexOf("no.")==0 || text.indexOf("no,")==0){ sendMessage("I'm sorry, I must have misunderstood. Please tell me again."); } } function getMessage(){ var messages = document.querySelectorAll("div[gv-test-id='message-item-container']"); var latest = messages[messages.length-1].children[1].children[0].children[0].children[1].children[1].innerText; console.log(latest); parseMessage(latest); loading = false; } function fire(el, etype){ var evt = new Event(etype, {"bubbles":true, "cancelable":false, "composed": true}); el.dispatchEvent(evt); }
This script allows Santa to get new messages and parse them for children's names and specific keywords. He can also reply with a handful of random responses in context to the children's behavior. And that's the magic! The parent texts Santa an update on one or both kids and he replies with some words of inspiration. The meter will also update based on Santa keeping track of his naughty and nice list.
Conclusion
While I may pursue future updates, but this is probably the end of this project for me. Unless someone I know asks me to build one, I likely won't do much about my hard-coded kids names. I just wanted to share this with anyone who thought it was a good idea and wanted some help in accomplishing something similar. Enjoy some more photos below that I took when assembling a bit.