(27/6/2026)
Let's command line test accessing TTN data form slan-093 in the polytunnel in Drumgeely..
pi@drumgeely:~ $
pi@drumgeely:~ $
pi@drumgeely:~ $ date
Sat 27 Jun 14:06:54 IST 2026
pi@drumgeely:~ $ uptime
14:06:57 up 18 days, 10:27, 3 users, load average: 0.21, 0.11, 0.04
pi@drumgeely:~ $ df -m .
Filesystem 1M-blocks Used Available Use% Mounted on
/dev/mmcblk0p2 14351 12563 1040 93% /
pi@drumgeely:~ $
pi@drumgeely:~ $ mosquitto_sub -h ###URL### -p 1883 -u ###USER### -P ###PASSWORD### -t \# -F %I%J
2026-06-27T14:13:44+0100{"tst":"2026-06-27T14:13:44.715422Z+0100","topic":"v3/tola-park-environment@ttn/devices/slan-093/up","qos":0,"retain":0,"payloadlen":1712,"payload":{"end_device_ids":{"device_id":"slan-093","application_ids":{"application_id":"tola-park-environment"},"dev_eui":"70B3D57ED0069C07","join_eui":"0000000000000000","dev_addr":"260BE272"},"correlation_ids":["gs:uplink:01KW4KDX844SM3HXK9TNR7JCAD"],"received_at":"2026-06-27T13:13:44.658361992Z","uplink_message":{"session_key_id":"AZ2DKWrsvLI34SF6BYC4fw==","f_port":8,"f_cnt":10863,"frm_payload":"FS4TUAABizYNyAAAAHsQiA==","decoded_payload":{"battery":3528,"battery_level":0,"gas_resistance":8065.16,"humidity":49.44,"pressure":1011.74,"temperature":24.22},"rx_metadata":[{"gateway_ids":{"gateway_id":"stt-drumgeely-gateway-id","eui":"B827EBFFFEBC60E1"},"time":"2026-06-27T13:13:44.356957912Z","timestamp":1593785500,"rssi":-97,"channel_rssi":-97,"snr":11.25,"location":{"latitude":52.699723918918075,"longitude":-8.8875639438629168,"source":"SOURCE_REGISTRY"},"uplink_token":"CiYKJAoYc3R0LWRydW1nZWVseS1nYXRld2F5LWlkEgi4J+v//rxg4RCc+fz3BRoMCIiZ/9EGEOeW2tcBIOCCk6ixmQc=","received_at":"2026-06-27T13:13:44.363999711Z"}],"settings":{"data_rate":{"lora":{"bandwidth":125000,"spreading_factor":9,"coding_rate":"4/5"}},"frequency":"867100000","timestamp":1593785500,"time":"2026-06-27T13:13:44.356957912Z"},"received_at":"2026-06-27T13:13:44.453013498Z","confirmed":true,"consumed_airtime":"0.226304s","version_ids":{"brand_id":"heltec","model_id":"cubecell-1-2-aa-node-class-a-otaa","hardware_version":"_unknown_hw_version_","firmware_version":"1.0","band_id":"EU_863_870"},"network_ids":{"net_id":"000013","ns_id":"EC656E0000000181","tenant_id":"ttn","cluster_id":"eu1","cluster_address":"eu1.cloud.thethings.network"},"last_battery_percentage":{"f_cnt":10769,"value":0,"received_at":"2026-06-26T21:28:24.174897133Z"}}}}
So lets put this into a Python script next..
pi@drumgeely:~ $ mkdir mqtt_python3
pi@drumgeely:~ $ python3 -m venv ./mqtt_python3
pi@drumgeely:~ $ source ./mqtt_python3/bin/activate
(mqtt_python3) pi@drumgeely:~ $ pwd
/home/pi
(mqtt_python3) pi@drumgeely:~ $ cd mqtt_python3/
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ pip3 install paho-mqtt
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting paho-mqtt
Obtaining dependency information for paho-mqtt from https://www.piwheels.org/simple/paho-mqtt/paho_mqtt-2.1.0-py3-none-any.whl.metadata
Downloading https://www.piwheels.org/simple/paho-mqtt/paho_mqtt-2.1.0-py3-none-any.whl.metadata (23 kB)
Downloading https://www.piwheels.org/simple/paho-mqtt/paho_mqtt-2.1.0-py3-none-any.whl (67 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 67.2/67.2 kB 504.2 kB/s eta 0:00:00
Using cached https://www.piwheels.org/simple/paho-mqtt/paho_mqtt-2.1.0-py3-none-any.whl (67 kB)
Installing collected packages: paho-mqtt
Successfully installed paho-mqtt-2.1.0
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
And write the script..
Got from https://www.emqx.com/en/blog/how-to-use-mqtt-in-python
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ cat ./test.py
from paho.mqtt import client as mqtt_client
import random
broker = '###URL###'
port = 1883
topic = '#' # Want everything so escape \ for \# topic
client_id = f'python-mqtt-{random.randint(0, 1000)}'
username = '###USERNAME###'
password = '###PASWORD###'
def connect_mqtt() -> mqtt_client:
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print("Connected to MQTT Broker!")
else:
print(f"Failed to connect, return code {reason_code}")
client = mqtt_client.Client(
client_id=client_id,
callback_api_version=mqtt_client.CallbackAPIVersion.VERSION2,
)
client.username_pw_set(username, password)
client.on_connect = on_connect
client.connect(broker, port)
return client
def subscribe(client: mqtt_client):
def on_message(client, userdata, msg):
print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic")
client.subscribe(topic)
client.on_message = on_message
def run():
client = connect_mqtt()
subscribe(client)
client.loop_forever()
if __name__ == '__main__':
run()
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ python3 ./test.py
Connected to MQTT Broker!
Received `{"end_device_ids":{"device_id":"slan-093","application_ids":{"application_id":"tola-park-environment"},"dev_eui":"70B3D57ED0069C07","join_eui":"0000000000000000","dev_addr":"260BE272"},"correlation_ids":["gs:uplink:01KW4PWM05KPRKH14ACZ6AENDP"],"received_at":"2026-06-27T14:14:12.437386512Z","uplink_message":{"session_key_id":"AZ2DKWrsvLI34SF6BYC4fw==","f_port":8,"f_cnt":10869,"frm_payload":"FJoEFQABi7kNygAAAEKGCg==","decoded_payload":{"battery":3530,"battery_level":0,"gas_resistance":4359.69,"humidity":10.45,"pressure":1013.05,"temperature":22.740000000000002},"rx_metadata":[{"gateway_ids":{"gateway_id":"stt-drumgeely-gateway-id","eui":"B827EBFFFEBC60E1"},"time":"2026-06-27T14:14:12.135459899Z","timestamp":926595667,"rssi":-84,"channel_rssi":-84,"snr":8,"location":{"latitude":52.699723918918075,"longitude":-8.887563943862917,"source":"SOURCE_REGISTRY"},"uplink_token":"CiYKJAoYc3R0LWRydW1nZWVseS1nYXRld2F5LWlkEgi4J+v//rxg4RDT9Oq5AxoLCLS1/9EGEKGdu20guKif6/uCCA==","received_at":"2026-06-27T14:14:12.102755295Z"}],"settings":{"data_rate":{"lora":{"bandwidth":125000,"spreading_factor":7,"coding_rate":"4/5"}},"frequency":"868500000","timestamp":926595667,"time":"2026-06-27T14:14:12.135459899Z"},"received_at":"2026-06-27T14:14:12.230395754Z","confirmed":true,"consumed_airtime":"0.071936s","version_ids":{"brand_id":"heltec","model_id":"cubecell-1-2-aa-node-class-a-otaa","hardware_version":"_unknown_hw_version_","firmware_version":"1.0","band_id":"EU_863_870"},"network_ids":{"net_id":"000013","ns_id":"EC656E0000000181","tenant_id":"ttn","cluster_id":"eu1","cluster_address":"eu1.cloud.thethings.network"},"last_battery_percentage":{"f_cnt":10769,"value":0,"received_at":"2026-06-26T21:28:24.174897133Z"}}}` from `v3/tola-park-environment@ttn/devices/slan-093/up` topic
Now lets store this to the sqlite3 database :)
mqtt_python3) pi@drumgeely:~/mqtt_python3 $ ls -al
total 36
drwxr-xr-x 5 pi pi 4096 Jun 27 15:28 .
drwx------ 23 pi pi 4096 Jun 27 15:06 ..
drwxr-xr-x 2 pi pi 4096 Jun 27 15:00 bin
-rw-r--r-- 1 pi pi 1 Jun 27 15:27 database.db
drwxr-xr-x 3 pi pi 4096 Jun 27 14:59 include
drwxr-xr-x 3 pi pi 4096 Jun 27 14:59 lib
-rw-r--r-- 1 pi pi 160 Jun 27 14:59 pyvenv.cfg
-rw-r--r-- 1 pi pi 1262 Jun 27 15:14 test_mqtt.py
-rw-r--r-- 1 pi pi 360 Jun 27 15:28 test_sqlite3.py
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ python3 ./test_sqlite3.py
User: Tobias, Age: 28
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ ls -al database.db
-rw-r--r-- 1 pi pi 8192 Jun 27 15:28 database.db
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open database.db
sqlite> .databases
main: /home/pi/mqtt_python3/database.db r/w
sqlite> .tables
users
sqlite> .schema
CREATE TABLE users (name TEXT, age INTEGER);
sqlite> .exit
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ date
Sat 27 Jun 15:34:08 IST 2026
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $ sqlite3 database.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite> SELECT * from users;
Tobias|28
sqlite> .exit
(mqtt_python3) pi@drumgeely:~/mqtt_python3 $
So we can now write user data to the user table in the databases.db file
Let's decide the table structure to store our Drumgeely sensor data
sensors - name of table
index name (20 chars), time (40 chars), data (JSON data upto 200 chars)
'slan-093 ', '2026-06-27T14:14:12.230395754Z', '{"battery":3530,"battery_level":0,"gas_resistance":4359.69,"humidity":10.45,"pressure":1013.05,"temperature":22.740000000000002}'
So 260 chars per table entry
Let's try that in real life..
(26/6/2026 09:48)
So I couldn't install plotly on drumgeely Rpi (see below) so instead I will capture sensor data locally to a sqlite database and then do 5 minute updates to graphs held on calendula Rpi (which in turn are shared on Shannon Town Community Wetlands website).
Let's start by accessing LoRaWAN sensor data using Python and MQTT ..
First let's log in remotely using a Hyper terminal shell and update the system..
Linux drumgeely 6.12.75+rpt-rpi-v7 #1 SMP Raspbian 1:6.12.75-1+rpt1~bookworm (2026-03-11) armv7l
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Jun 20 15:18:24 2026 from 172.28.15.43
pi@drumgeely:~ $
pi@drumgeely:~ $ sudo apt update
Get:1 http://raspbian.raspberrypi.com/raspbian bookworm InRelease [15.0 kB]
Get:2 http://archive.raspberrypi.com/debian bookworm InRelease [55.0 kB]
After this let's check on disk space and uptime..
(20/6/2026 12:30)
So in this blog we will provide remotely accessible graphs on a Raspberry Pi. The RPi in question is in the 40' container of Shannon Tidy Towns in Drumgeely. I anticipate accomplishing this by the following steps:
1) Get a NoIp domain name installed on the Rpi (noip2)
2) Install webserver (apache2)
3) Install graphing software (dash_plotly)
4) Install database (sqlite3)
5) Gather (TTN, MQTT, WiFi) and post data to graph to the database
6) Created graph web pages
Most of the above has already been done by me on my RPi smart home host called calendula. So let's go through the stages...
I have an account (kilnageer@gmail.com) at https://my.noip.com/ and a number of free domains already registered. So let's use snntt57.zapto.org (which was last updated as 86.41.249.104 on Jun 17, 2026 09:27:10)
So using SSH over Zerotier I can talk to this remote Rpi and see what code it is running.
pi@drumgeely:~ $ df -m .
Filesystem 1M-blocks Used Available Use% Mounted on
/dev/mmcblk0p2 14351 12017 1585 89% /
pi@drumgeely:~ $ sudo systemctl status apache2
Unit apache2.service could not be found.
pi@drumgeely:~ $
It has less than 2GB left on its 16GB microSD card and no Apache2 webserver running.
pi@drumgeely:~ $ sudo apt install apache2
pi@drumgeely:~ $ date
Sat 20 Jun 12:43:20 IST 2026
pi@drumgeely:~ $
pi@drumgeely:~ $ sudo systemctl status apache2.service
● apache2.service - The Apache HTTP Server
Loaded: loaded (/lib/systemd/system/apache2.service; enabled; preset: enab>
Active: active (running) since Sat 2026-06-20 12:32:45 IST; 11min ago
Docs: https://httpd.apache.org/docs/2.4/
Main PID: 26473 (apache2)
Tasks: 55 (limit: 1556)
CPU: 337ms
CGroup: /system.slice/apache2.service
├─26473 /usr/sbin/apache2 -k start
├─26475 /usr/sbin/apache2 -k start
└─26476 /usr/sbin/apache2 -k start
Jun 20 12:32:45 drumgeely systemd[1]: Starting apache2.service - The Apache HTT>
Jun 20 12:32:45 drumgeely apachectl[26472]: AH00558: apache2: Could not reliabl>
Jun 20 12:32:45 drumgeely systemd[1]: Started apache2.service - The Apache HTTP>
pi@drumgeely:~ $
I then tried accessing this RPi using its Zerotier IP address and I saw the Apache2 landing page. So step 2) above achieved.
Next to install the noip client that will periodically inform the No-Ip server which IP address is current assigned to this Drumgeely RPi
https://my.noip.com/dynamic-dns/duc - no good for RPi
https://raspberrytips.com/install-no-ip-raspberry-pi/ - OK
So had to build and install for Rpi using above instructions then run to perform an IP update..
pi@drumgeely:~/noip/noip-2.1.9-1 $ sudo noip2 -c /usr/local/etc/no-ip2.conf
Can then see the update at:
https://my.noip.com/dns/records?jump_to_zone=zapto.org
snntt57 A 188.65.190.74 60 Jun 20, 2026 13:00:20
###TODO Will need to make this noip2 command into a systemctl service.
But the laptop web browser still doe not get the Apache2 landing page at:
http://snntt57.zapto.org/
So may need to map port 22 through the Tenda router. Unfortunately, after logging in to the Tenda portal, via the Android app, I could see the Tenda_D986A0 device and the drumgeely Rpi and Solas WiFi dongle but the portal app gave no port mapping options.
###TODO So may need to log in to this router locally to do this! And assign a static LAN address to the drumgeely Rpi.
Or log into the drumgeely Rpi over VNC and then launch a browser to the router at 192.168.0.1
Set static DHCP address and DMZ but still cannot remote access Apache2 server. (Also set DDNS to noip http://snntt57.zapto.org/)###PROBLEM still cannot remotely access via http://snntt57.zapto.org/ buut OK via Zerotier IP address
###SOLUTION webscrape graphical pages and republish to ShannonTownWetlands.ie website for public access.
So I will park this up for now and get the graphing software installed..
https://dash.plotly.com/installation
pi@drumgeely:~ $ mkdir dash_plotly
pi@drumgeely:~ $ python3 -m venv ./dash_plotly/
pi@drumgeely:~ $ source ./dash_plotly/bin/activate
(dash_plotly) pi@drumgeely:~ $
(dash_plotly) pi@drumgeely:~ $ pip install dash
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
###PROBLEM After lots of timeouts installing I then got a runtime error..
(dash_plotly) pi@drumgeely:~/dash_plotly $ python ./app.py
Traceback (most recent call last):
File "/home/pi/dash_plotly/./app.py", line 2, in <module>
import plotly.express as px
File "/home/pi/dash_plotly/lib/python3.11/site-packages/plotly/express/__init__.py", line 12, in <module>
raise ImportError(
ImportError: Plotly Express requires numpy to be installed. You can install numpy using pip with:
$ pip install numpy
Or install Plotly Express and its dependencies directly with:
$ pip install "plotly[express]"
You can also use Plotly Graph Objects to create a large number of charts without installing
numpy. See examples here: https://plotly.com/python/graph-objects/
(dash_plotly) pi@drumgeely:~/dash_plotly $
Same code runs OK on calendula Rpi!
Tried editing xxxxx/_init_.py see:
https://stackoverflow.com/questions/69866554/trying-to-import-plotly-express-but-get-this-error-even-though-pandas-is-install
but didn't help
Moved on to install sqlite3
https://www.tech-reader.blog/2025/08/installing-and-verifying-sqlite-on.html
pi@drumgeely:~ $ sudo apt install sqlite3
pi@drumgeely:~ $ sqlite3 --version
3.40.1 2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f3alt1
pi@drumgeely:~ $