The following pages describe an IoT build that transforms a heater into a device that can be remotely controlled via Alexa. Additionally, the build will use a temperature sensor that will be build-in and will report temperature readings to a base where some level of automation can be performed.
Similar to most Wizardry and Steamworks builds, rather than automating the devices, data is centralized and the actual automation is performed using software rather than hardware. In doing so, some of the portability is lost but quick an inexpensive changes can be made in software rather than in hardware.
For this build, we looked for a small heating device that could heat up a small room, not too expensive but sturdy enough to be effective. One good candidate at about USD14 is a small heater that has a lot of five star on Amazon:
The heater is also incidentally analog with the on-off switching consisting in a button whose behavior can be substituted with a relay. Digital builds have been made before, and consisted in using a transistor switch to make the digital contact but analog switches are best to emulate with ease with a very simple relay.
Finally, the heater chosen is not too complex, with too many controls that would have to be rewired and remotely accessed, and as a bonus, the heater also is delivered with a tilt mechanism that makes the heater turn off in case the case falls over.
Disassembling the heater is very easy involving only some pressure that must be applied around the oval-shaped ring that literally just holds the entire inner-assembly in place. With the colored oval ring popped out, all the interior of the heater can be accessed.
The first noticeable component is the giant "heat resistor" as a giant grill, with wires running across the top. The heater is supposed to have two stages of heating that can be activated by a button on the back of the heater, each of these stages offering more heat than the other.
Interestingly, the build is extremely simple: the grill up front is electrified via the top wires hating up and a micro-controller (the small PCB) powers a simple fan that just blows the heat through the grill and into the room. The switch that selects the two speeds, simply heats the grill partly or fully thereby achieving more heat.
On the bottom of the heater is the "tilt mechanism" that turns the heater off if it has fallen over. The "mechanism", in quotes because it does not include a typical sensor, similarly has a simple design: a momentary switch (a microswitch) pushes a button that is pressed whenever the heater stands on its bottom foot and when the button is depressed, the momentary switch breaks the connection.
The first worry in realizing this project is to think about how power will be drawn in order to power the circuitry. Intuitively, a good guess would be to latch onto the PCB that the heater already has, and correctly so, because the PCB is just a drop-down AC-DC converter that converters mains voltage to to power the rear fan. However, the PCB is only powered whenever the heater is actually operating, being behind the pressure-switch, as well as the main switch on the back, which means that an ESP micro-controller would not be live to receive the command in other to turn the heater on or off.
Unfortunately, there is no easy solution with this heater, such that mains voltage is pulled directly off the main power cable of the heater. Needless to say that the inner circuitry of the heater relies on some plastic isolator cups to quickly make the connection between several wires, such that all the wires have to be released from the plastic isolator cups, tied with the additional wires (black, in the image), then soldered together, isolated with heat-shrink tubing and finally secured with plastic glue.
Now that the mains power supply cables have been pulled, a step-down and converter is needed to change mains voltage to , respectively as required by micro-controllers and digital circuits. One cute solution, and not really a hack is to use a small travel USB charger as a step-down.
USB chargers with a single slot are capable of up to one, two or more amperes without being too bulky. Typically, either an U.S. or an European charger would be great due to U.K. chargers being more bulky due to the additional fuse.
Gutting these chargers should be performed with care! Due to space restrictions the plastic casing is typically molded tightly around the electronics such that it is very easy to damage the circuit when trying to pry open the plastic casing.
Either way, when the plastic is removed, the circuitry is very straight-forward, with a bridge AC-DC converter, a transformer that separates the AC and DC circuits, feeding a rather banal USB slot that typically is used to charge any USB device. It is perhaps more tidy to remove the USB port and then connect leads to the freshly desoldered pads of the USB port but another solution is to flip the converter over and then solder wires to the pin connectors of the USB port. The converter used for this project was from a BOSH power-tool and incidentally happened to have some decently large solder pads, aside from the USB port, but the easiest is still hooking onto the pins of the USB port.
As illustrated, some hot-melt glue is then used to seal the connections and is really meant to just increase the tensile strength when the heater will have to be mounted back and cables will have to be stuffed back into the casing.
As usual, the next step is to decide on the ESP to use. The usual conundrum of choice between the ESP-01S and the battle-proven ESP8266 comes up, with the ESP32 always and as usual being considered an excessive choice for such builds. Due to the use of sensors, and the GPIO requirements, the WeMoS ESP8266 wins again, being an affordable development kit that is so cheap it can be bought in bulk and without further ado, the circuit is created by pulling , ground and a GPIO (at first) for the relay.
A perforated PCB is chosen as the base to build the circuitry on, even though the various components will be separated and dispersed through the heater enclosure, just for the easy-of-use and for the ability to gut-and-reuse the components if need be. For instance, the WeMoS ESP8266 is mounted on rails, being able to be detached at short notice. Next, more leads are pulled for the heat sensor that will also be connected to the WeMoS.
One cool way to add some isolation at low-cost is to use painter's tape with electrical/thermal properties to wrap up the components thereby ensuring no stray contact takes place between components or exposed wires. In the image, the blue tape masks the USB AC-DC converter and works both as a fire-retardant as well as an electrical isolator.
With all components connected, some testing is performed to ensure that everything works correctly. This is done by first toggling the relay on and off using just command-line tools and a corresponding Arduino sketch, and then by attempting to retrieve the temperature from the DHT11 sensor.
Putting everything back together becomes an arduous task, with the various cables not behaving and poking out from everywhere, making us regret that the chosen wires had not been chosen to be longer, but eventually everything is successfully squashed inside the casing.
One remarkable thing about the heater is that there is, in fact, lots of space inside the case. Furthermore, one thought was to eliminate the momentary switch altogether and use a sensor for the tilt, which is a good solution that would produce an enormous amount of space on the bottom of the heater.
Fortunately, the heater is designed with a collimator of sorts, consisting in a plastic piece that gets laid over the resistor and is meant to focus the heat emission to the outside of the heater, thereby not allowing heat to escape on the side of the grill. This is great, because there are a lot of wires around the heater that should not be heated or overheated, along with the new WeMoS controller and the relay. Albeit not pictured, the temperature sensor is slid to the back of the heater, next to the fan such that the sensor will remain unimpressed by the generated heat.
The outer grill can now be assembled, and the oval ring pressed back into place, holding the heater together. Finally, that marks the end of the hardware part of the assembly, with a pretty good looking result.
Now Alexa has someone to heat her up!
Here are the various software components used in this build.
The Arduino sketch is just a blend between the pin-toggle template and the DHT11 polling template being nothing too novel.
/*************************************************************************/ /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ /*************************************************************************/ // Removing comment for debugging over the first serial port. // #define DEBUG 1 // The AP to connect to via Wifi. #define STA_SSID "" // The AP Wifi password. #define STA_PSK "" // The MQTT broker to connect to. #define MQTT_HOST "" // The MQTT broker username. #define MQTT_USERNAME "" // The MQTT broker password. #define MQTT_PASSWORD "" // The MQTT broker port. #define MQTT_PORT 1883 // The default MQTT client ID is "esp-CHIPID" where CHIPID is the ESP8266 // or ESP32 chip identifier. #define MQTT_CLIENT_ID() String("esp-" + String(GET_CHIP_ID(), HEX)) // The authentication password to use for OTA updates. #define OTA_PASSWORD "" // The OTA port on which updates take place. #define OTA_PORT 8266 // The default topic that the sketch subscribes to is "esp/CHIPID" where // CHIPID is the ESP8266 or ESP32 chip identifier. #define MQTT_TOPIC() String("esp/" + String(GET_CHIP_ID(), HEX)) // Platform specific defines. #if defined(ARDUINO_ARCH_ESP8266) #define GET_CHIP_ID() (ESP.getChipId()) #elif defined(ARDUINO_ARCH_ESP32) #define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32)) #endif // Miscellaneous defines. //#define CHIP_ID_HEX (String(GET_CHIP_ID()).c_str()) #define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX)) // Platform specific libraries. #if defined(ARDUINO_ARCH_ESP8266) #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #elif defined(ARDUINO_ARCH_ESP32) #include <WiFi.h> #include <ESPmDNS.h> #endif // General libraries. #include <WiFiUdp.h> #include <ArduinoOTA.h> #include <PubSubClient.h> #include <ArduinoJson.h> #if defined(ARDUINO_ARCH_ESP32) #include <FS.h> #include <SPIFFS.h> #endif // DHT11 #include <Adafruit_Sensor.h> #include <DHT.h> #include <DHT_U.h> // Uncomment the type of sensor in use: #define DHTTYPE DHT11 // DHT 11 //#define DHTTYPE DHT22 // DHT 22 (AM2302) //#define DHTTYPE DHT21 // DHT 21 (AM2301) WiFiClient espClient; PubSubClient mqttClient(espClient); // Define GPIO pins for supported architectures. #if defined(ARDUINO_ARCH_ESP8266) int PINS[] = { D0, D1, D2, D3, D4, D5, D6, D7, D8 }; #elif defined(ARDUINO_ARCH_ESP32) int PINS[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39 }; #endif String mqttSerialize(StaticJsonDocument<256> msg) { char output[256]; serializeJson(msg, output, 256); return String(output); } void mqttCallback(char *topic, byte *payload, unsigned int length) { String msgTopic = String(topic); // payload is not null terminated and casting will not work char msgPayload[length + 1]; snprintf(msgPayload, length + 1, "%s", payload); #ifdef DEBUG Serial.println("Message received on topic: " + String(topic) + " with payload: " + String(msgPayload)); #endif // Parse the payload sent to the MQTT topic as a JSON document. StaticJsonDocument<256> doc; #ifdef DEBUG Serial.println("Deserializing message...."); #endif DeserializationError error = deserializeJson(doc, msgPayload); if (error) { #ifdef DEBUG Serial.println("Failed to parse MQTT payload as JSON: " + String(error.c_str())); #endif return; } // Do not process messages without an action key. if (!doc.containsKey("action")) { return; } String action = (const char *)doc["action"]; if (action == "set") { String state = (const char *)doc["state"]; const int pin = (const int)doc["pin"]; #ifdef DEBUG Serial.println("Setting pin: " + String(pin) + " to state: " + String(state)); #endif pinMode(PINS[pin], OUTPUT); if (state == "on") { digitalWrite(PINS[pin], HIGH); int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif return; } digitalWrite(PINS[pin], LOW); int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif return; } if (action == "get") { const int pin = (const int)doc["pin"]; #ifdef DEBUG Serial.println("Getting pin: " + String(pin) + " state."); #endif int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif // Announce the action. StaticJsonDocument<256> msg; msg["pin"] = pin; switch (status) { case 1: msg["state"] = "on"; break; case 0: msg["state"] = "off"; break; default: msg["state"] = "unknown"; break; } mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); return; } sensors_event_t event; if (action == "temperature") { const int pin = (const int)doc["pin"]; StaticJsonDocument<256> msg; msg["id"] = String(MQTT_CLIENT_ID().c_str()); DHT_Unified dht(PINS[pin], DHTTYPE); dht.begin(); // Print temperature sensor details. dht.temperature().getEvent(&event); switch (!isnan(event.temperature)) { case true: msg["temperature"] = event.temperature; break; default: Serial.println("Error reading temperature..."); // Eeek? //dht.end(); return; } // Publish the sensor data. mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); // Eeek? //dht.end(); return; } if (action == "humidity") { const int pin = (const int)doc["pin"]; StaticJsonDocument<256> msg; msg["id"] = String(MQTT_CLIENT_ID().c_str()); DHT_Unified dht(PINS[pin], DHTTYPE); dht.begin(); dht.humidity().getEvent(&event); switch (!isnan(event.relative_humidity)) { case true: msg["humidity"] = event.relative_humidity; break; default: Serial.println("Error reading humidity..."); // Eeek? //dht.end(); return; } // Publish the sensor data. mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); // Eeek? //dht.end(); return; } } bool mqttConnect() { #ifdef DEBUG Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST)); #endif mqttClient.setServer(MQTT_HOST, MQTT_PORT); StaticJsonDocument<256> msg; if (mqttClient.connect(MQTT_CLIENT_ID().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) { #ifdef DEBUG Serial.println("Established connection with MQTT broker using client ID: " + MQTT_CLIENT_ID()); #endif mqttClient.setCallback(mqttCallback); msg["action"] = "connected"; mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); #ifdef DEBUG Serial.println("Attempting to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif if (!mqttClient.subscribe(MQTT_TOPIC().c_str())) { #ifdef DEBUG Serial.println("Failed to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif return false; } #ifdef DEBUG Serial.println("Subscribed to MQTT topic: " + MQTT_TOPIC()); #endif msg.clear(); msg["action"] = "subscribed"; mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); return true; } #ifdef DEBUG Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state())); #endif return false; } bool loopWifiConnected() { // Process OTA loop first since emergency OTA updates might be needed. ArduinoOTA.handle(); // Process MQTT client loop. if (!mqttClient.connected()) { // If the connection to the MQTT broker has failed then sleep before carrying on. if (!mqttConnect()) { return false; } } mqttClient.loop(); return true; } void setup() { Serial.begin(115200); #ifdef DEBUG Serial.println("Booted, setting up Wifi in 10s..."); #endif delay(10000); WiFi.mode(WIFI_STA); #if defined(ARDUINO_ARCH_ESP8266) WiFi.hostname(HOSTNAME().c_str()); #elif defined(ARDUINO_ARCH_ESP32) WiFi.setHostname(HOSTNAME().c_str()); #endif WiFi.begin(STA_SSID, STA_PSK); while (WiFi.waitForConnectResult() != WL_CONNECTED) { #ifdef DEBUG Serial.println("Failed to connect to Wifi, rebooting in 5s..."); #endif delay(5000); ESP.restart(); } #ifdef DEBUG Serial.print("Connected to Wifi: "); #endif Serial.println(WiFi.localIP()); #ifdef DEBUG Serial.println("Setting up OTA in 10s..."); #endif delay(10000); // Port defaults to 8266 ArduinoOTA.setPort(OTA_PORT); // Hostname defaults to esp-[ChipID] ArduinoOTA.setHostname(HOSTNAME().c_str()); // Set the OTA password ArduinoOTA.setPassword(OTA_PASSWORD); ArduinoOTA.onStart([]() { switch (ArduinoOTA.getCommand()) { case U_FLASH: // Sketch #ifdef DEBUG Serial.println("OTA start updating sketch."); #endif break; #if defined(ARDUINO_ARCH_ESP8266) case U_FS: #elif defined(ARDUINO_ARCH_ESP32) case U_SPIFFS: #endif #ifdef DEBUG Serial.println("OTA start updating filesystem."); #endif SPIFFS.end(); break; default: #ifdef DEBUG Serial.println("Unknown OTA update type."); #endif break; } }); ArduinoOTA.onEnd([]() { #ifdef DEBUG Serial.println("OTA update complete."); #endif SPIFFS.begin(); #if defined(ARDUINO_ARCH_ESP8266) // For what it's worth, check the filesystem on ESP8266. SPIFFS.check(); #endif ESP.restart(); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { #ifdef DEBUG Serial.printf("OTA update progress: %u%%\r", (progress / (total / 100))); #endif }); ArduinoOTA.onError([](ota_error_t error) { #ifdef DEBUG Serial.printf("OTA update error [%u]: ", error); #endif switch (error) { case OTA_AUTH_ERROR: #ifdef DEBUG Serial.println("OTA authentication failed"); #endif break; case OTA_BEGIN_ERROR: #ifdef DEBUG Serial.println("OTA begin failed"); #endif break; case OTA_CONNECT_ERROR: #ifdef DEBUG Serial.println("OTA connect failed"); #endif break; case OTA_RECEIVE_ERROR: #ifdef DEBUG Serial.println("OTA receive failed"); #endif break; case OTA_END_ERROR: #ifdef DEBUG Serial.println("OTA end failed"); #endif break; default: #ifdef DEBUG Serial.println("Unknown OTA failure"); #endif break; } ESP.restart(); }); ArduinoOTA.begin(); // Set up MQTT client. mqttClient.setServer(MQTT_HOST, MQTT_PORT); mqttClient.setCallback(mqttCallback); // Touchdown. #ifdef DEBUG Serial.println("Setup complete."); #endif } void loop() { // Check the Wifi connection status. int wifiStatus = WiFi.status(); switch (wifiStatus) { case WL_CONNECTED: if (!loopWifiConnected()) { delay(1000); break; } delay(1); break; case WL_NO_SHIELD: #ifdef DEBUG Serial.println("No Wifi shield present."); #endif goto DEFAULT_CASE; break; case WL_NO_SSID_AVAIL: #ifdef DEBUG Serial.println("Configured SSID not found."); #endif goto DEFAULT_CASE; break; // Temporary statuses indicating transitional states. case WL_IDLE_STATUS: case WL_SCAN_COMPLETED: delay(1000); break; // Fatal Wifi statuses trigger a delayed ESP restart. case WL_CONNECT_FAILED: case WL_CONNECTION_LOST: case WL_DISCONNECTED: default: #ifdef DEBUG Serial.println("Wifi connection failed with status: " + String(wifiStatus)); #endif DEFAULT_CASE: delay(10000); ESP.restart(); break; } }
The Node-Red sketch has some exotic nodes due to Amazon Alexa being used, which requires some sort of bridge to Amazon itself, but the rest just relies on built-in nodes.
The "timestamp" and "template" cluster just implement the the pin-toggle template semantics that ar then delivered to the MQTT bus that the Arduino sketch subscribes to; with the "timestamp" node just being a manual override for triggering the controls.
The bottom "timestamp" and "template" pair are made to query the temperature, by delivering a JSON payload with the format:
{ "pin": "3", "action": "temperature" }
which is the only particularity of the povided Arduino sketch.
Perhaps the most interesting thing for this template is that the lower "timestamp" node is set to re-deliver the above JSON payload on repeat to the MQTT bus, thereby querying the temperature from the Arduino sketch and WeMoS inside the heater. The WeMoS responds with the temperature and the result is fed-back into the Node-Red Smart Home Control node.
As usual, any further automation will be created within Node-Red instead of modifying the Arduino sketch. With the "temperature" being known, it now becomes trivial to automate the heating based on the perceived temperature. The WeMoS could be made to turn on when the temperature drops below a set value and then turn off when another maximal value is exceeded.
[{"id":"23e0eab953f3e600","type":"tab","label":"Heater","disabled":false,"info":"","env":[]},{"id":"9d0a67ae24cb18bf","type":"debug","z":"23e0eab953f3e600","name":"debug 39","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":440,"y":580,"wires":[]},{"id":"d0521f600a3e2f06","type":"mqtt in","z":"23e0eab953f3e600","name":"","topic":"esp/f7189f","qos":"2","datatype":"auto-detect","broker":"5b0ca401533c273c","nl":false,"rap":true,"rh":0,"inputs":0,"x":260,"y":400,"wires":[["21cb9c024377ad17"]]},{"id":"25fc79fcb56aef44","type":"mqtt out","z":"23e0eab953f3e600","name":"","topic":"esp/f7189f","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"5b0ca401533c273c","x":830,"y":400,"wires":[]},{"id":"310b2a845b87b159","type":"template","z":"23e0eab953f3e600","name":"","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n \"pin\": \"2\",\n \"state\": \"on\",\n \"action\": \"set\"\n}","output":"json","x":640,"y":300,"wires":[["25fc79fcb56aef44"]]},{"id":"fb7d4e7cc367bf69","type":"inject","z":"23e0eab953f3e600","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":640,"y":260,"wires":[["310b2a845b87b159"]]},{"id":"d449f50f666c45ae","type":"template","z":"23e0eab953f3e600","name":"","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n \"pin\": \"2\",\n \"state\": \"off\",\n \"action\": \"set\"\n}","output":"json","x":640,"y":380,"wires":[["25fc79fcb56aef44"]]},{"id":"fac633ad41adfc24","type":"inject","z":"23e0eab953f3e600","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":640,"y":340,"wires":[["d449f50f666c45ae"]]},{"id":"32adb0275b659dd2","type":"inject","z":"23e0eab953f3e600","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":650,"y":420,"wires":[["4cfd396ea20e4a03"]]},{"id":"4cfd396ea20e4a03","type":"template","z":"23e0eab953f3e600","name":"","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n \"pin\": \"3\",\n \"action\": \"temperature\"\n}","output":"json","x":640,"y":460,"wires":[["25fc79fcb56aef44"]]},{"id":"14206957491d2d0b","type":"alexa-smart-home-v3","z":"23e0eab953f3e600","conf":"646e7cbc924ee6e0","device":"61533","acknowledge":true,"name":"Bedroom Heating","topic":"","x":240,"y":320,"wires":[["9d0a67ae24cb18bf","fcfafacae0af5d97"]]},{"id":"fcfafacae0af5d97","type":"switch","z":"23e0eab953f3e600","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"ON","vt":"str"},{"t":"eq","v":"OFF","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":430,"y":320,"wires":[["310b2a845b87b159"],["d449f50f666c45ae"],[]]},{"id":"93b4e6fa6de53537","type":"alexa-smart-home-v3-state","z":"23e0eab953f3e600","conf":"646e7cbc924ee6e0","device":"61533","name":"Bedroom Heating","x":670,"y":520,"wires":[]},{"id":"11c3d49880026590","type":"debug","z":"23e0eab953f3e600","name":"debug 40","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":640,"y":580,"wires":[]},{"id":"21cb9c024377ad17","type":"change","z":"23e0eab953f3e600","name":"","rules":[{"t":"set","p":"acknowledge","pt":"msg","to":"true","tot":"bool"},{"t":"set","p":"payload.state","pt":"msg","to":"{}","tot":"jsonata"},{"t":"set","p":"payload.state.temperature","pt":"msg","to":"payload.temperature","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":520,"wires":[["11c3d49880026590","93b4e6fa6de53537"]]},{"id":"5b0ca401533c273c","type":"mqtt-broker","name":"","broker":"iot","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"646e7cbc924ee6e0","type":"alexa-smart-home-v3-conf","username":"","mqttserver":"mq-red.cb-net.co.uk","webapiurl":"red.cb-net.co.uk","contextName":"memory"}]
The tilt sensor is a very simple way to determine if the heater has fallen over, consisting in a simple button with a microswitch, but given the ESP addition, it is fairly trivial to remove the entire tilt mechanism and replace it with a tilt sensor. One very cool sensor that seems to fit the build and budget is a very simple tilt sensor such as the KY-020 tilt sensor.
The KY-020 tilt sensor is based on a ball that is contained within the sensor that will roll and create a contact whenever the sensor is tilted. There are better solutions than the KY-020 ball sensor, for instance a full gyroscopic sensor that can detect tilt on the three axes but that is definitely overkill for such a project.