This gist answers a Discord question. It explains my approach to collecting CPU temperatures by sending the data as an MQTT payload.
I like to think of this approach as working with the MING (Mosquitto, InfluxDB, Node-RED, Grafana) paradigm rather than fighting against it (eg using approaches like SSH calling out of the Node-RED container).
The approach is not tied to the Raspberry Pi that is running IOTstack. If you have several Raspberry Pis, they can all log their temperatures to the Raspberry Pi running IOTstack using exactly the same mechanism.
The concept can also be extended to other types of computer. I'm using it to collect CPU temperatures for macOS systems too.
Be very careful about copying and pasting text from this gist. Unless you take precautions, Windows will add its CR+LF line-endings and those will cause problems.
-
Assumed path name:
~/.local/bin/publish_rpi_temperature
-
Script content:
#!/usr/bin/env bash mkdir -p ~/Logs mosquitto_pub -h mosquitto.mydomain.com -t "/mytopic/computer/pi" -m "{\ \"host\": \"$HOSTNAME\", \ \"temp\": $(vcgencmd measure_temp | cut -c 6-9)\ }"
The script assumes the Mosquitto clients are installed:
$ sudo apt install mosquitto-clients
You will need to customise:
- The target host "mosquitto.mydomain.com" using the fully-qualified domain name, or host name, or multicast DNS name, or IP address of the Raspberry Pi running IOTstack.
- The topic "/mytopic/computer/pi". Although I did not need to use it, I originally added the "/pi" suffix to the topic string in case I needed to know the type of computer that had generated the payload. My Raspberry Pi's use "/pi"; my macOS machines "/mac".
This script is specific to the Raspberry Pi. To port the script to a different type of host you will need to:
- find an alternative for
vcgencmd
; and - make sure that the HOSTNAME environment variable contains a sensible value (eg on macOS, it contains the fully-qualified domain name rather than the host name).
The MQTT messages sent by this script look like this:
/mytopic/computer/mac {"host": "bauxite", "temp": 46.5}
/mytopic/computer/mac {"host": "magnetite", "temp": 28.8}
/mytopic/computer/mac {"host": "gravel", "temp": 35.2}
/mytopic/computer/mac {"host": "marble", "temp": 34.2}
/mytopic/computer/pi {"host": "octopi", "temp": 41.3}
/mytopic/computer/pi {"host": "new-dev", "temp": 43.3}
/mytopic/computer/pi {"host": "iot-hub", "temp": 46.2}
/mytopic/computer/pi {"host": "sec-dev", "temp": 45.2}
$ mkdir -p ~/Logs
The collection script does not normally produce output unless there is an error condition. If the script does not appear to be working, the place to start looking is:
$ tail ~/Logs/publish_rpi_temperature.log
-
Preamble (should be common to all crontab files):
SHELL=/bin/bash HOME=/home/pi PATH=/home/pi/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-
Event trigger:
# report system temperature every 5 minutes */5 * * * * publish_rpi_temperature >>./Logs/publish_rpi_temperature.log 2>&1
The InfluxDB database needs to be initialised before you can write to it. In this example, the database name is "computer".
$ docker exec -it influxdb influx
> create database computer
> exit
$
Within the "computer" database, each host computer logging temperatures generates its own series:
temperature,system=«hostname»
where "«hostname»" corresponds with the "host" key in the MQTT payload.
A Node-RED three-node flow handles the messages:
-
an MQTT-in node subscribing to the appropriate topic. In this example the topic is "/mytopic/computer/+" where "+" is a single-level wildcard.
-
a Change node to prepare the payload for insertion into InfluxDB:
[ { "temp": payload.temp },{ "system": payload.host } ]
In words:
- temperature is added as a field (data);
- the name of the host reporting the temperature is added as a tag (metadata)
-
an InfluxDB-out node inserts the prepared payload into the database named "computer".
Given the incoming MQTT message:
/mytopic/computer/pi {"host": "iot-hub", "temp": 46.2}
The overall effect of the flow is:
> USE computer
> INSERT temperature,system='iot-hub' temp=46.2
> exit
You should be able to copy the JSON content below and use the "Import" command in the main menu (three horizontal bars "≡" at the top, right of the Node-Red window) to create a new flow containing the three nodes.
Depending on how your system is set up, the flow may need some tinkering before it works.
[
{
"id": "cc5ffba5.fdc7c8",
"type": "tab",
"label": "CPU Temperatures",
"disabled": false,
"info": ""
},
{
"id": "796d8452.225294",
"type": "change",
"z": "cc5ffba5.fdc7c8",
"name": "prepare fields",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "[\t {\t \"temp\": payload.temp\t },{\t \"system\": payload.host\t }\t]\t",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 100,
"wires": [
[
"d27128f3.4050c8"
]
]
},
{
"id": "d27128f3.4050c8",
"type": "influxdb out",
"z": "cc5ffba5.fdc7c8",
"influxdb": "626e8bc5.4c702c",
"name": "write to InfluxDB",
"measurement": "temperature",
"precision": "",
"retentionPolicy": "",
"database": "",
"retentionPolicyV18Flux": "",
"org": "",
"bucket": "",
"x": 570,
"y": 100,
"wires": []
},
{
"id": "39543010.e0258",
"type": "mqtt in",
"z": "cc5ffba5.fdc7c8",
"name": "/mytopic/computer/+",
"topic": "/mytopic/computer/+",
"qos": "2",
"datatype": "json",
"broker": "c5d29fb5.89907",
"x": 130,
"y": 100,
"wires": [
[
"796d8452.225294"
]
]
},
{
"id": "626e8bc5.4c702c",
"type": "influxdb",
"hostname": "influxdb",
"port": "8086",
"protocol": "http",
"database": "computer",
"name": "",
"usetls": false,
"tls": "",
"influxdbVersion": "1.x"
},
{
"id": "c5d29fb5.89907",
"type": "mqtt-broker",
"name": "Docker MQTT",
"broker": "mosquitto",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": false,
"protocolVersion": 4,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": ""
}
]
$ docker exec -it influxdb influx -precision=rfc3339
Connected to http://localhost:8086 version 1.8.9
InfluxDB shell version: 1.8.9
> use computer
Using database computer
> show series
key
---
temperature,system=bauxite
temperature,system=gravel
temperature,system=iot-hub
temperature,system=magnetite
temperature,system=marble
temperature,system=new-dev
temperature,system=octopi
temperature,system=sec-dev
temperature,system=sjgair
> select * from temperature where time > now() - 10m tz('Australia/Sydney')
name: temperature
time system temp
---- ------ ----
2021-08-22T23:40:00.271972208+10:00 marble 35.2
2021-08-22T23:40:00.674021388+10:00 magnetite 29.6
2021-08-22T23:40:00.937395236+10:00 gravel 36.6
2021-08-22T23:40:01.065496792+10:00 bauxite 46
2021-08-22T23:40:01.220100822+10:00 sec-dev 46.7
2021-08-22T23:40:01.87568606+10:00 iot-hub 47.7
2021-08-22T23:40:01.892511832+10:00 octopi 40.8
2021-08-22T23:40:02.021640681+10:00 new-dev 42.8
2021-08-22T23:45:00.210631999+10:00 magnetite 28.9
2021-08-22T23:45:00.453033759+10:00 gravel 35.6
2021-08-22T23:45:00.545580449+10:00 bauxite 46.4
2021-08-22T23:45:00.840558319+10:00 marble 35.6
2021-08-22T23:45:01.133290607+10:00 new-dev 43.3
2021-08-22T23:45:01.319572925+10:00 sec-dev 46.2
2021-08-22T23:45:01.977801647+10:00 iot-hub 46.7
2021-08-22T23:45:01.99638355+10:00 octopi 41.3
> exit
$
Shows a mixture of Raspberry Pi and macOS CPU temperatures.
I will break it down.
The
#
at the start means "this is a comment" so, in principle, the rest of the line does not matter.The combination of
#!
is a special kind of comment which is treated as a hint to the calling shell as to how it should launch the script.Traditionally, you would see something like this at the start of any shell script:
where
sh
could besh
or orbash
orzsh
orksh
or any other kind of shell (there are an awful lot of shells).Now, let's make two assumptions:
bash
. That's the default on a Raspberry Pi.fred.sh
) in a way that causesbash
to execute it.If you don't have a
#!
hint thenbash
(the running shell) assumes the script should be executed with a copy of itself.The problem is that all shells differ in their precise features. You might, therefore, create a script which depends on it being run by
sh
. It might fail or do the wrong thing if it is run bybash
. This is where the#!
hint comes in. Iffred.sh
starts with:then it doesn't matter whether the shell you are actually running is
sh
orbash
or anything else. When the shell you are running startsfred.sh
it will always start it withsh
.All clear so far?
But then comes another problem. What happens if you have multiple versions of a particular shell installed? This can happen for any number of reasons. You might be testing a new version of a shell. Or, if you are a macOS user who favours
bash
overzsh
, the version ofbash
Apple ships by default is 3.2.57 while up-to-datebash
(which you get from HomeBrew) is around 5.1.8.I'm a Mac person but I want up-to-date
bash
so I wind up with both versions installed:Do you see where this is heading? Suppose my
fred.sh
script starts with:That's going to tell the calling shell (which is actually
/usr/local/bin/bash
) to use/bin/bash
which is, of course, the obsolete version so it might not work if I use features infred.sh
that are only present in the up-to-date version ofbash
.And that's where this syntax comes in:
The
env
program looks at thebash
argument and says "go find the version ofbash
which is actually in use", then use that version to run thefred.sh
script. So, the practical effect on my Mac is the same as if the script had started with:The reason for using the
env bash
syntax rather than hard-coding/usr/local/bin/bash
is to make the script portable. As long as there's abash
installed on the target system (eg Raspberry Pi),env
will find it and use it.Multiple versions of shells being installed on a system, or multiple potential installation locations depending on the platform (macOS vs Linux vs something else), and automatically figuring out which is the correct shell to use when starting a script, is a very common problem. The solution of
/usr/bin/env
plus an argument of the shell name to use to run the script is a pattern that all shell scripts are supposed to adopt. Granted, not every example you find on the web will use this convention - but they should.Make sense?
With all that in mind,
/usr/bin/env
is a file, not a directory:It's a compiled binary program:
If you want to know more about how it works:
If you are not familiar with Unix text editors, you could try this:
Make sure the target directory exists:
Make that directory your working directory:
Start this command (which will appear to do nothing - don't panic):
Select everything in the box below and copy it to your clipboard:
Switch back to the terminal where that
cat
command from step 3 is still waiting and paste the contents of the clipboard.Hold down the control key, then press the d key. Control+D means "end input". The terminal "$" prompt should come back.
Check your work by listing the file you just created:
The output should be the same as the box above. If it isn't, repeat from step 3.
Also note the subtle difference between the
cat
command here and the one in step 3. Step 3 has the>
so it says "read from the keyboard and write to the file namedpublish_rpi_temperature
". The command here in step 7 doesn't have the>
so it says "read from the file namedpublish_rpi_temperature
and write to the console".Make the script executable:
After that, it should work properly.