# How to build a wireless mailbox notifier with temperature sensor and dashboard **<p style="text-align: right;"><sub>Stefan Nilsson - sn223gc</sub></p>** :::info This project shows how to log and send sensor data over LoRa/WiFi, and store it in the cloud to notify the user when a specified value is triggered. As an example, I'll use a magnetic contact as a trigger and log temperature readings at intervals. All data will be visualized in a dashboard. To extend battery life, the device will go into deepsleep between readings. ::: **Estimated price:** 93 € **Estimated time:** All my problems and solutions are mentioned below, so with the tutorial and code at hand, I estimate that the project will take 8-10 hours to complete. --- ## Objective ![Overview image](https://i.imgur.com/6EULn92.jpg) The idea came about to solve a first world problem. I get deliveries infrequently, so instead of having to check the box daily, I wanted to get notified whenever there's a delivery. To make it low-maintenance, the original idea was to make it solar-powered, but due to its surroundings and the realization that Pycom's hardware is quite efficient, I came to the conclusion that a battery would suffice if I made use of deepsleep and limited awake time. Overall I reckon it's a useful project to learn about the IoT infrastructure which can then be applied to all future projects. It makes use of WiFi, LoRa, TTN, Node-RED, MariaDB, Grafana, config files, deepsleep, battery monitoring, email notifications, and sensor readings. --- ## Material ![Material](https://i.imgur.com/G7NXJ1I.jpg) I originally purchased Elegoo 37 in 1 Sensor Modules Kit which included the sensors I needed for prototyping. KY-025 as a magnetic trigger and DHT11 as a temperature and humidity sensor. When the project matured, I went out and purchased better protected sensors. I reckon this is a good idea to keep the costs down before you know how viable your project is and once the project is finalized, you can use them again for your next project. I also used a rechargeable Lithium Ion battery to avoid wasting batteries when I modified the battery monitoring. | | Store | Price € | |:---------------------------------- |:--------- | -------:| | Pycom LoPy4 | pycom.io | 35 | | Pycom Expansion Board v3.1 | pycom.io | 16 | | Pycom LoRa Antenna | pycom.io | 8 | | Pycom Pycase clear | m.nu | 4 | | 3 x AAA Battery Holder (2-Pin JST) | m.nu | 4 | | AM2302 temperature-humidity sensor | m.nu | 22 | | HAA28 mini reed switch | kjell.com | 4 | | Breadboard | amazon.de | - | | Wires | amazon.de | - | | Micro USB cable | | - | | | | 93 € | Be aware that prices vary heavily between vendors, so try to order well in advance. AM2302 would have cost me a fraction of the price if I wasn't in a rush to get the delivery. **LoPy4** is a MicroPython programmable development board using ESP32 chipset. Pycom has a wide range of development boards with the main difference being their network options. LoPy comes with Bluetooth, WiFi, LoRa, and Sigfox. One thing to note is that their documentation recommends that you always attach the antenna to avoid damaging the device when using LoRa. **Expansion Board** provides you with easy access pin connections, 2-Pin JST connection for a battery, battery charger, USB-connection for code transfer and power, RGB LED (useful for displaying status), as well as a MicroSD card slot (not used in this project). **Battery Holder** was selected for its easy-access outside of the Pycom case. It's a lot easier to replace three AAA batteries than taking the whole device indoors to charge it. **AM2302** is a wired DHT22 temperature sensor in a plastic case. It uses 3.3-5.5V and delivers data through a digital signal. The operating temperature is -40~80 Celsius with an accuracy of ±0.5 Celsius and a resolution of 0.1 Celsius. Seemingly more accurate than DHT11. **HAA28** is reed switch which turns on/off when in contact with a magnet. I'll go into the details below. The specs online says gray but I'd argue that they are white. Tools used to finalize the project: * Wire cutter * Wire stripper * Soldering iron * Shrink tubing * Heat gun * Dremel --- ## Computer & hardware setup With a new Pycom device, it's recommended to update both the device and expansion board with latest firmware to get bug fixes and new features. I recommend Pycom's documentation for details on how to do this since it changes between versions and operating systems. * [Updating expansion board firmware](https://docs.pycom.io/pytrackpysense/installation/firmware/) * [Updating Pycom device firmware](https://docs.pycom.io/gettingstarted/installation/firmwaretool/) On Windows, updating the expansion board turned out to be a major pain. You had to install third party software, download the firmware update to the same folder, hold a button when connecting LoPy, enter DFU-mode, find the device and click update within seven seconds, and hope that you selected the right driver. Eventually I skipped it and moved on. Updating the LoPy firmware was a lot easier. I installed their software, made sure to exit all other software communicating with LoPy (IDE), and then clicked through their update wizard. To write code and communicate with the device over USB, I'm using [Visual Studio Code](https://code.visualstudio.com/) with Pycom's plugin [Pymakr](https://marketplace.visualstudio.com/items?itemName=pycom.Pymakr) and [Node.js](https://nodejs.org/en/). For a complete guide on the setup, see [Pycom's tutorial](https://docs.pycom.io/pymakr/installation/vscode/). --- ## Putting everything together ![Wiring](https://i.imgur.com/Smblzop.jpg) The **battery holder** is connected to the 2-Pin JST. My holder is using three AAA batteries connected in series to provide 3x1.5 = 4.5 volts. :::warning **Note:** A comment on Pycom's forum claimed that there's no wiring standard for the 2-Pin JST connection, so verify that the battery holder matches your device. ::: The **LoRa antenna** is connected to the 868/915 MHz port. As mentioned above, Pycom recommends that you always attach the antenna to avoid damaging the device when using LoRa. **AM2302** temperature sensor is connected with red to 3V3, black to GND and yellow (data) to P23. **HAA28** reed switch is connected so one wire goes to GND and the other goes to P22. We don't have to differentiate the wires and both of mine were white. The sensors came without pins, so I sacrificed a couple of wires and cut them in half so I could solder the pins onto the sensors. If you're new to soldering, remember to put on shrink tubing before soldering because it won't go over the pins. If you're using crocodile clips as a pair of extra hands, make sure to cover the jaws in tape or they will damage the wire. When soldering, heat the wire and apply the solder directly to the wire - not the soldering iron! Lastly I used a Dremel to cut an extra hole in the back of the Pycase so I can use both a battery and USB cable at the same time. This is to avoid having to open the case every time you want to attach it to a computer or add/remove/replace/charge the battery. ![Pycase](https://i.imgur.com/otBC6CO.jpg) ### Two wires going into one GND connection During the development I handled this by using a breadboard. Instead of plugging AM2302 and HAA28 directly into GND, you wire them to the blue minus column on your breadboard and then wire a new cable from the same column to the GND on your LoPy. When it was time to finalize the project, I split two black wires and soldered a y split that connects the two sensors to GND. ![Y-split wire](https://i.imgur.com/aJplcfl.jpg) ### HAA28 A [reed switch](https://en.wikipedia.org/wiki/Reed_switch) is operated by applying a magnetic field. It works by having two flexible magnetic metal contacts that are separated by a small gap when there is no magnet present. A magnetic field will then make the two metal contacts attract and complete the circuit. The switch itself doesn't provide us with a value, it's just in open or closed state. So when using Pycom's [Pin()](https://docs.pycom.io/firmwareapi/pycom/machine/pin/), the value (HIGH/LOW) depends on how we connect the switch (GND vs 3V3). We also need to make use of Pycom's internal resistor to avoid [floating input](http://www.thebox.myzen.co.uk/Tutorial/Inputs.html) when the state is open. Since I'm using GND, it means that the value is LOW when the magnet is near and HIGH when the magnet is far away. This in turn means that [machine.pin_sleep_wakeup()](https://docs.pycom.io/firmwareapi/pycom/machine/) should be set to wake on machine.WAKEUP_ANY_HIGH. I'm also setting the input pin to use PULL_UP. ![Reed switch](https://i.imgur.com/MWrgueR.jpg) While building the project, I found other types of reed switches that are open when near a magnetic field. They would likely be better suited to reduce power. :::warning **Note:** Use tape to strap the magnet and switch so they don't trigger during development. ::: ### Battery calculations The math to estimate average power consumption is quite simple: ![Average current formula](https://i.imgur.com/jtzvXbY.gif) The challenge is to get proper numbers. Pycom's datasheet for LoPy4 claims 105 mA during LoRa transmit and 19.5 μA during deepsleep. AM2302 is 1-1.5 mA during measurement and 40-50 μA when idle. I can't find numbers on the expansion board. Since I didn't have access to measuring tools, I decided to estimate a worst case scenario based on figures posted on forums. * <200 mA during awake with wifi, extension board and multiple sensors. * <100 μA during deepsleep with extension board. When running on WiFi, I measured an average awake cycle to be 14 seconds with the main villain being WiFi connection and transfer. If I measure temperature once every hour, this would give me a power consumption of <0.88 mA. If I measure once every four hours, then the average power consumption would drop to <0.49 mA. Awake time is your worst enemy. Which is one reason why LoRa is helpful for this project. By switching to LoRa and saving the connection, the awake time drops to 10 seconds. It gives an average consumption of <0.66 mA when measuring every hour, and <0.38 mA when measuring every four hours. The next step is to calculate battery life: ![Battery life formula](https://i.imgur.com/aErpbzU.gif) mAh for AAA varies but on average it seems to be 1000 mAh. Since the batteries are connected in series to boost voltage, it's still 1000 mAh. This gives the LoRa device 63 days of power when measuring every hour. **If we measure once every four hours, it would last 109 days**. We could also make use of the internal file system and store nightly values to send in the morning. :::warning **Note:** since everything is based on estimated generic values and I don't account for awake time when the device is triggered or discharge rate, it will be different in reality. ::: --- ## Platform My criteria for the platform were: free, low-maintenance, notifications, and infinite data storage. The original prototype was set up using [Pybytes](https://pybytes.pycom.io/) which was helpful for getting started but only retains your data for 30 days and was limited in what I could do with the data. There's no built in notifications or filters so all data was sent to your specified webhook. [Ubidots](https://ubidots.com/) offers a lot more functionality but their free tier has the same limit of only retaining data for 30 days. At this stage, I considered using [urequests.py](https://github.com/micropython/micropython-lib/blob/master/urequests/urequests.py) to call a webhook on IFTTT to forward the data to Google Sheets. Fortunately we then had our lecture on [Node-RED](https://nodered.org/), which is basically an extended version of the old Yahoo Pipes. You can place and customize nodes in whatever order you want to modify data and there are plenty of open-source plugins to extend it with new functionality. [Their tutorial]() is great. To keep it low-maintaince, I opted for using it for free on [IBM Cloud](https://nodered.org/docs/getting-started/ibmcloud). IBM offered 1 GB of data storage but it seemed that I was only allowed to run one instance on their cloud with the free tier. So I couldn't run both a database and Node-RED. My solution was to send all data to an existing SQL database running at Loopia (so no additional cost). Node-RED plugins used: * [node-red-contrib-stackhero-mysql](https://flows.nodered.org/node/node-red-contrib-stackhero-mysql) to insert SQL data * [node-red-contrib-ttn](https://flows.nodered.org/node/node-red-contrib-ttn) to receive data from The Things Network * [node-red-node-email](https://flows.nodered.org/node/node-red-node-email) to send email notifications The setup went without major issues. TTN has a [quick start guide](https://www.thethingsnetwork.org/docs/applications/nodered/quick-start.html) on using their plugin and I had to create an [app password](https://support.google.com/accounts/answer/185833?hl=en) to be able to connect to the Gmail account. ![Node-RED Flow](https://i.imgur.com/2gLAC7q.png) <p style="text-align: right; font-style: italic;"><sup><a href="https://i.imgur.com/2gLAC7q.png">Click here to open the full-size image</a></sup></p> :::warning **Note:** Make sure to set up a response in Node-RED when sending data through POST or LoPy will be stop and wait until timed out. That was approximately 20 seconds. ::: As you can see, one of the advantages with Node-RED is that you can set up multiple inputs for the same flow. So I was able to develop the flow by injecting placeholder data until it was fully working and I could extend it with a connection to my LoPy over WiFi, and then LoRa. --- ## The code [The full code is published on GitHub](https://github.com/nilste/mailbox). To be blunt, the code is a mess. It grew as I learned and is in desperate need of refactoring. The code is extensively commented though if you want to learn or make changes. There are some design decisions worth mentioning: A **JSON file** to store network settings. This is primarily to be able to use .gitignore to avoid uploading private information but also to make it easier for the user to update them. I considered moving all settings to it, but the JSON format doesn't allow explaining comments. **Error handling** with Try/Except is used to make sure that the device continues to run and doesn't stop if there's an issue with WiFi. Rather a missed reading than a crash. **Power consumption** was in focus, so Bluetooth is turned off in boot[]().py, deepsleep is used, LoRa saves its connection, and network libraries will not be imported if they aren't used. **Voltage measurements** are done through the expansion board's built in voltage divider on P16. ### File structure ``` project ┣ lib ┃ ┣ dht.py ┃ ┣ system.py ┃ ┗ urequests.py ┣ boot.py ┣ config-example.json ┣ config.json ┗ main.py ``` ### Libraries * [dht.py](https://github.com/JurassicPork/DHT_PyCom) * [system.py](https://github.com/hiveeyes/terkin-datalogger/blob/master/src/lib/terkin/sensor/system.py) (simplified) * [urequests.py](https://github.com/micropython/micropython-lib/tree/master/urequests) ### config.json ```python= { "USE_WIFI": false, "SSID": "Wifi SSID", "SSID_PASS": "WiFi pass", "HTTP_URL": "localhost", "HTTP_PASSWORD": "password", "USE_LORA": false, "APP_EUI": "0000000000000000", "APP_KEY": "00000000000000000000000000000000" } ``` ### boot[]().py ```python= from network import Bluetooth import pycom pycom.heartbeat(False) # Disable bluetooth to save power bluetooth = Bluetooth() bluetooth.deinit() ``` ### main[]().py ```python= import machine # Used for deepsleep import time # Used for sleep import json # Used for config file import system # Used for voltage from machine import Pin from dht import DHT resetReason = machine.reset_cause() wakeReason = machine.wake_reason()[0] # (wake_reason, gpio_list) ### SETTINGS ### pycom.rgbled(0xFFFF00) # Yellow # Sensor used for measuring temperature # DHT11: 0, DHT22: 1 DHTType = 1 DHTPin = 'P23' # Reed switch used to wake the device reedPin = 'P22' # Internal voltage divider used for measuring battery voltage # These values are from Pycom Extension Board v3.1 batteryPin = 'P16' resistorR1 = 1000 resistorR2 = 1000 batteryAttn = 6.0 # Deep sleep settings # Mode can be machine.WAKEUP_ALL_LOW or machine.WAKEUP_ANY_HIGH # enable_pull decides if pull up / down resistors should be enabled during sleep secondsToSleep = 60 * 60 * 4 wakePins = ['P22'] wakeMode = machine.WAKEUP_ANY_HIGH wakePull = True ### NETWORK ### with open('config.json') as file: config = json.load(file) if config['USE_LORA']: from network import LoRa import socket import ubinascii import struct # Initialise LoRa in LORAWAN mode. lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868) if resetReason == machine.DEEPSLEEP_RESET: print('Woke from deepsleep...') lora.nvram_restore() else: print('Connecting to LoRa...') app_eui = ubinascii.unhexlify(config['APP_EUI']) app_key = ubinascii.unhexlify(config['APP_KEY']) lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) while not lora.has_joined(): print('Not yet joined...') pycom.rgbled(0xcc00ff) time.sleep(1) pycom.rgbled(0x000000) time.sleep(0.5) print("Joined network") # Create a LoRa socket s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # Set the LoRaWAN data rate s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5) elif config['USE_WIFI']: print('Connecting to WiFi...') from network import WLAN import urequests as requests try: wlan = WLAN(mode=WLAN.STA) nets = wlan.scan() for net in nets: if net.ssid == config['SSID']: print('Network found!') wlan.connect(net.ssid, auth=(net.sec, config['SSID_PASS']), timeout=5000) while not wlan.isconnected(): machine.idle() # Save power while waiting print('WLAN connection succeeded!') break except: print('Was not able to connect to WiFi...') ### SENSOR SETUP ### print('Initiating sensors...') pycom.rgbled(0x0000FF) # Blue dhtsensor = DHT(Pin(DHTPin, mode=Pin.OPEN_DRAIN), DHTType) reedsensor = Pin(reedPin, mode=Pin.IN, pull=Pin.PULL_UP) sysvolt = system.SystemVoltage(batteryPin, resistorR1, resistorR2, batteryAttn) machine.pin_sleep_wakeup(pins=wakePins, mode=wakeMode, enable_pull=wakePull) ### SENSOR HANDLING ### if wakeReason == machine.PIN_WAKE: print('Pin triggered!') pycom.rgbled(0xFF0000) # Red # Send notification if config['USE_LORA']: print('Sending over LoRa...') s.setblocking(True) s.bind(2) # Define port s.send(bytes([0x01])) s.setblocking(False) elif config['USE_WIFI'] and wlan.isconnected(): print('Sending over WiFi...') try: payload = { "password": config['HTTP_PASSWORD'], "triggered": True } headers = { "Content-Type": "application/json" } res = requests.post(config['HTTP_URL'], headers = headers, json = payload) res.close() except: print('An error occured when trying to send data...') # Avoid sending multiple notifications while reedsensor.value(): machine.idle() # Save power while waiting elif wakeReason == machine.RTC_WAKE or resetReason == machine.WDT_RESET or resetReason == machine.PWRON_RESET: print('Sleep time ended') print('Fetching and sending data...') pycom.rgbled(0x00FF00) # Green # Fetch data from DHT sensor dhtresult = dhtsensor.read() while not dhtresult.is_valid(): time.sleep(.5) dhtresult = dhtsensor.read() # Fetch values tempValue = dhtresult.temperature humiValue = dhtresult.humidity battValue = sysvolt.read() # Print values print('Temp: ', tempValue) print('RH: ', humiValue) print('Volt: ', battValue) # Send values if config['USE_LORA']: print('Sending over LoRa...') s.setblocking(True) s.bind(1) # Define port # Multiply with 100 to avoid having to send floats payload = struct.pack('>h', int(tempValue * 100)) payload += struct.pack('>H', int(humiValue * 100)) payload += struct.pack('>H', int(battValue * 100)) s.send(payload) s.setblocking(False) elif config['USE_WIFI'] and wlan.isconnected(): print('Sending over WiFi...') try: payload = { "password": config['HTTP_PASSWORD'], "temperature": tempValue, "humidity": humiValue, "voltage": battValue } headers = { "Content-Type": "application/json" } res = requests.post(config['HTTP_URL'], headers = headers, json = payload) res.close() except: print('An error occured when trying to send data...') ### GO TO SLEEP ### print('Preparing deepsleep...') pycom.rgbled(0x000000) # Turn off if (config['USE_LORA']): lora.nvram_save() elif config['USE_WIFI']: wlan.deinit() # Avoid getting wifi timeout next cycle sleepInterval = 0 remainingSleepTime = machine.remaining_sleep_time() # Milliseconds if (remainingSleepTime > 0): sleepInterval = remainingSleepTime else: sleepInterval = int(secondsToSleep * 1000) machine.deepsleep(sleepInterval) ``` --- ## Transmitting the data / connectivity ![Network](https://i.imgur.com/DCdI6tD.jpg) [LoRa](https://en.wikipedia.org/wiki/LoRa) is a long-range low-powered [physical layer](https://en.wikipedia.org/wiki/Physical_layer) makes it well-suited for this project both in terms of battery consumption and range. To utilize it, we use [LoRaWAN](https://lora-alliance.org/) as protocol, and a [network server](https://www.thethingsnetwork.org/docs/network/architecture.html) that routes data to our application over TCP/IP. The world's largest open LoRa network is [The Things Network](https://www.thethingsnetwork.org/) (TTN). Unfortunately their coverage is spotty and my closest gateway is 15 km away. It's not expensive to purchase your own gateway though and this project is mainly about learning, so I'll be using both LoRa and WiFi. Data will be sent when LoPy is triggered by opening the box (delivery). It will also send temperature, humidity and voltage once every fourth hour. ### LoRaWAN To use TTN we must first create an account, add an application, and register our device. Pietro Manzoni has written a [great tutorial](https://hackmd.io/@lnu-iot/B12k-ampU) on doing that. When you've completed that and Node-RED is configured with the TTN plugin mentioned earlier, then we can begin sending data. TTN has a [Fair Access Policy](https://www.thethingsnetwork.org/docs/lorawan/duty-cycle.html) limiting how much data we can send. To follow it, we will transform our floats into integers by multiplying them by 100 and using int(). Then we use Python's [struct module](https://docs.python.org/2/library/struct.html) to turn it into binary data. TTN has an in-depth tutorial on [working with bytes](https://www.thethingsnetwork.org/docs/devices/bytes.html). ```python= payload = struct.pack('>h', int(tempValue * 100)) payload += struct.pack('>H', int(humiValue * 100)) payload += struct.pack('>H', int(battValue * 100)) ``` At the backend of TTN, we also need a decoder to tell them what to do with our data and convert the integers back to float values before sending it to Node-RED. ```python= function Decoder(bytes, port) { var decoded = {}; switch(port) { case 1: var temperature = (bytes[0] << 8) | bytes[1]; var humidity = (bytes[2] << 8) | bytes[3]; var voltage = (bytes[4] << 8) | bytes[5]; decoded.temperature = (temperature / 100); decoded.humidity = humidity / 100; decoded.voltage = voltage / 100; break; case 2: decoded.triggered = bytes[0]; break; } return decoded; } ``` We could make it even more effective by truncating the data instead of preserving the floats. ### WiFi WiFi is a lot simpler and uses urequests[]().py to send a JSON payload with POST to Node-RED. Example from main[]().py on how to do that: ```python= payload = { "password": config['HTTP_PASSWORD'], "temperature": tempValue, "humidity": humiValue, "voltage": battValue } headers = { "Content-Type": "application/json" } res = requests.post(config['HTTP_URL'], headers = headers, json = payload) res.close() ``` The first JSON field is the password I'm using to verify the data within Node-RED. WiFi doesn't come with the same limitations as LoRaWAN which is why I'm sending it as regular JSON. The drawbacks are the higher battery consumption and shorter range. :::warning **Note:** I have a WiFi extender running with the same name as my regular network. This seemed to cause a naming conflict when connecting and I had to unplug it. ::: --- ## Presenting the data ![Grafana dashboard](https://i.imgur.com/q8Ff243.jpg) As mentioned above, I settled on using MariaDB as an SQL server at Loopia to store the data. The reasons were that it came with no additional cost to me, I'm experienced with SQL, data is stored indefinitely, and I have complete control over it if I want to move or analyze it. The database tables can be created with the following: ```SQL= CREATE TABLE `deliveries` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, `date` timestamp NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE `measurements` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, `date` timestamp NOT NULL, `temperature` float NOT NULL, `humidity` float NOT NULL, `voltage` float NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; ``` Data about the delivery is saved when Node-RED receives a message that the LoPy has been triggered. Measurements are saved when Node-RED receives data containing voltage. To reduce the unlikely risk of getting bogus data from a third party, I'm also sending a password string from LoPy that Node-RED uses to verify the data before acting upon it. Email notifications are sent from Node-RED (via a dedicated Gmail account) when it receives the triggered message or once a day when the voltage level goes below a user specified value. ![Node-RED](https://i.imgur.com/2gLAC7q.png) <p style="text-align: right; font-style: italic;"><sup><a href="https://i.imgur.com/2gLAC7q.png">Click here to open the full-size image</a></sup></p> To visualize the data, I originally wrote a PHP script to fetch data from the SQL server and print it as a JSON file, followed by JavaScript that imports the JSON file and prints it to a HTML file with [Chart.js](https://www.chartjs.org/). This was a simple and free way to get up and running since my account at Loopia doesn't offer any way to set up Grafana. It was quite limited though with a static design. Then I found that [Grafana Cloud](https://grafana.com/products/cloud/) offers a free Starter Plan for a single user with up to five dashboards. This was more than enough for my current need so I quickly signed up, connected the MariaDB database as a data source and customized the dashboard to my liking. I was pleasantly surprised by how simple the process was and highly recommend it. --- ## Finalizing the design ![Finalized project](https://i.imgur.com/zI7fY7Q.jpg) {%youtube Cz__2pxRjh4 %} :::warning **Note:** I temporarily moved the reed switch pin to P4 (GPIO15) during the video and photo to disable Pycom's automatic launch debug message for the recording. ::: The project was tremendous fun, I learned a lot and the end result is ready to be deployed. There's room for improvement though. The code is in need of refactoring, battery consumption can be lowered by storing data locally and only transmit during daytime, the expansion board could be skipped by soldering better suited wires directly to LoPy, the reed switch could reduce its power by being replaced with a version that is in an open state when near a magnet, and finally I would love to create my own 3D printed case with a smaller footprint. It gave me a better understanding of IoT and I'm looking forward to my next project.