diff --git a/README.md b/README.md index f3390d1..766030c 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,59 @@ $ mosquitto_sub \ For an example how to insert the data into a database, see the documentation for [ruuviscan](https://github.com/susji/ruuviscan?tab=readme-ov-file#storing-temperature-values-in-an-sqlite-database). + +# Grabbing Ruuvi data from MQTT and exposing it as Prometheus metrics + +For a minimalistic example on how to expose Ruuvi values in Prometheus' +[text-based exposition +format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md) +see [scripts/ruuvimetrics.sh](scripts/ruuvimetrics.sh). The idea of the script +is this: We will again use `mosquitto_sub` to obtain Ruuvi messages, parse and +output them as JSON with `ruuviparse` and then generate a Prometheus-compatible +metrics file of the sensor values. In addition to this script, you will probably +need a way to serve the generated file over HTTP so Prometheus or a similar tool +can periodically read its contents. After this, it's trivial to consume the +sensor values with something like Grafana. + +In the script, you will want to modify the broker settings and the `ACT` +variable to copy the generated metrics file to your intended `TARGETDIR`. The +resulting file might look like something like this: + +``` +# Generated at Sun Apr 7 14:36:07 UTC 2024 by ruuvimetrics.sh +# TYPE ruuvi_temperature gauge +ruuvi_temperature{mac="aa:aa:aa:aa:aa:aa"} 5.185 1712500560000 +ruuvi_temperature{mac="bb:bb:bb:bb:bb:bb"} 2.615 1712500546000 +ruuvi_temperature{mac="cc:cc:cc:cc:cc:cc"} 2.865 1712500567000 +ruuvi_temperature{mac="dd:dd:dd:dd:dd:dd"} 22.289999 1712500567000 +# TYPE ruuvi_voltage gauge +ruuvi_voltage{mac="aa:aa:aa:aa:aa:aa"} 2.929 1712500560000 +ruuvi_voltage{mac="bb:bb:bb:bb:bb:bb"} 2.865 1712500546000 +ruuvi_voltage{mac="cc:cc:cc:cc:cc:cc"} 2.943 1712500567000 +ruuvi_voltage{mac="dd:dd:dd:dd:dd:dd"} 3.038 1712500567000 +# TYPE ruuvi_movement gauge +ruuvi_movement{mac="aa:aa:aa:aa:aa:aa"} 183 1712500560000 +ruuvi_movement{mac="bb:bb:bb:bb:bb:bb"} 125 1712500546000 +ruuvi_movement{mac="cc:cc:cc:cc:cc:cc"} 88 1712500567000 +ruuvi_movement{mac="dd:dd:dd:dd:dd:dd"} 154 1712500567000 +``` + +For testing the idea, something like this will suffice to serve the resulting +metrics file over HTTP: + + $ cd $TARGETDIR && python3 -m http.server 9200 --bind 127.0.0.1 + +If you think you need elevated privileges to run the script, you should instead +modify your target directory privileges accordingly. For some error-tolerance, +wrap the invocation in a loop like + +```sh +while true; do + echo "begin $(date)" + ./ruuvimetrics.sh + echo exited + sleep 10 +done +``` + +or use some kind of a process manager. diff --git a/scripts/ruuvimetrics.sh b/scripts/ruuvimetrics.sh new file mode 100644 index 0000000..bf623e2 --- /dev/null +++ b/scripts/ruuvimetrics.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +set -u + +BROKERHOST=broker.example.com +BROKERPORT=1883 +TOPIC=ruuvi/topic +CLIENTID=ruuvimetrics +AUTH='--cert cert.cert.pem --key key.private.key --cafile ca.crt' +F=metrics.txt +# Change this according to your setup. +ACT="/bin/cp $HOME/ruuvimetrics/${F} /var/www/htdocs/metrics" + +# Due to fromdateiso8601. +export TZ=UTC + +val() { + _mac="$1" + _kind="$2" + _ok="$3" + _val="$4" + _tmpf="$5" + echo "[$_mac] $_kind: $_val ($_ok)" + _p="ruuvi_${_kind}" + if [ "$_ok" = "true" ]; then + # We have a new valid value so filter out the old one. + _m="{mac=\"$_mac\"}" + cat "$F" | fgrep "${_p}{mac=" | fgrep -v "$_m" > "$_tmpf" + echo "${_p}${_m} $_val $_ts" >> "$_tmpf" + else + # No new valid value so just copy all we have. + cat "$F" | fgrep "${_p}{mac=" > "$_tmpf" + fi +} + +dump() { + _kind="$1" + _tmpf="$2" + { echo "# TYPE ruuvi_${_kind} gauge"; cat "$_tmpf"; } | sort >> "$F" +} + +activate() { + $ACT +} + +rm -f "$F" && touch "$F" +mosquitto_sub \ + -h "$BROKERHOST" \ + -p "$BROKERPORT" \ + -t "$TOPIC" \ + -i "$CLIENTID" \ + $AUTH \ + | jq --raw-output --unbuffered \ + '.ads[].ad | ascii_downcase | select(.[8:14] == "ff9904") | .[14:]' \ + | ruuviparse \ + | jq --raw-output --unbuffered \ + '. + { "OriginalTimestamp": .Timestamp } + | .Timestamp |= .[:index(".")] + "Z" + | .Timestamp |= fromdateiso8601 * 1000 + | . + {"CompareTimestamp": (.Timestamp / 1000 | todate) } + | [.OriginalTimestamp, + .Timestamp, + .MAC, + .Temperature.Valid, + .Temperature.Value, + .BatteryVoltage.Valid, + .BatteryVoltage.Value, + .MovementCounter.Valid, + .MovementCounter.Value] + | @tsv' \ + | while read -r _ots _ts _mac _vtemp _temp _vvolt _volt _vmove _move; do + echo "[$_mac] $_ots" + _ttemp=$(mktemp) + _tvolt=$(mktemp) + _tmove=$(mktemp) + val "$_mac" "temperature" "$_vtemp" "$_temp" "$_ttemp" + val "$_mac" "voltage" "$_vvolt" "$_volt" "$_tvolt" + val "$_mac" "movement" "$_vmove" "$_move" "$_tmove" + rm -f "$F" + echo "# Generated at $(date) by $(basename "$0")" > "$F" + dump "temperature" "$_ttemp" + dump "voltage" "$_tvolt" + dump "movement" "$_tmove" + activate + rm -f "$_ttemp" "$_tvolt" "$_tmove" + done