使用 InfluxDB 和 Grafana 监控水族箱
作者:Ben Tasker / 产品
2023 年 4 月 10 日
导航至
本文最初发布于 Ben Tasker 的博客,并已获得许可在此处转载。
我一直在设置一个新的热带鱼缸,并想添加一些监控和警报,因为,好吧,为什么不呢?
我感兴趣解答的关键问题是
- 过滤器是否正常运行?
- 温度是否在可接受的范围内?
- 计划中的事情(如表面撇渣器和灯)是否实际发生?
- 两个加热器是否都在工作?
计划还添加 PH 值水平的监控,但我需要的探针尚未到达。
在这篇文章中,我将讨论我使用 Raspberry Pi、InfluxDB、Telegraf 和 Grafana 构建的水族箱监控和警报系统。
指标来源
一些鱼缸健康要素可以通过我现有的 能源使用跟踪实施 公开的指标进行监控,这使我们能够查看哪些组件正在耗电(以及耗电量/时间)。
我需要做的只是将加热器等设备插入智能插座,并将它们添加到 我的 Docker 镜像 使用的配置中
kasa:
devices:
# ... etc ...
-
name: "aquarium-light"
ip: 192.168.5.165
-
name: "aquarium-surface-skimmer"
ip: 192.168.5.166
# ... etc ...
智能插座的使用还意味着 HomeAssistant 可以用作定时器,以安排表面撇渣器和水族箱灯等设备的运行
仅靠观察功耗是不够的,因为我们关心的一些指标需要进行物理测量。
例如,观察泵的能量消耗只是解决方案的一部分:如果叶轮发生故障,电机将继续耗电,同时无用地旋转。水族箱过滤器有许多故障模式,电机故障只是其中之一,我们真正关心的是泵实现的流速。
所以,我们需要一些传感器
使用的传感器是
- 基于 DS18B20 的温度探针(1-Wire 设备)
- Digitem FL-408 水流量计
显然,未来的 PH 值监控项目也需要自己的物理探针。
关于温度传感器的注意事项
如果您计划构建类似的东西,这里值得注意的是,从信誉良好的供应商(如 RS Components)购买温度探针非常重要。有 很多假的 DS18B20 芯片,它们往往会以令人讨厌和不可预测的方式出现故障。
上图显示的温度传感器是从亚马逊订购的,起初似乎工作正常。但是,在运行约 6 小时后,它养成完全脱离总线的习惯,除非 Pi 完全断电重启,否则不会重新出现。
显然,这种行为在劣质芯片中并不少见。它们也以返回一些非常可疑的读数而闻名
没错,如果相信探针,那么(在片刻之间)我的鱼缸温度从 ~20c 升至水临界温度的 3 倍…
一旦我意识到这个问题,我就从 DSRobot 订购了替换探针。它甚至比原来的更便宜:仿冒品不仅仅存在于价格范围的低端。
Raspberry Pi 构建
我有一个备用的 Raspberry Pi 4,自从它的 SD 卡故障以来就一直未使用(当时我将东西迁移到了其他地方),所以我决定使用它。但从理论上讲,任何配备 GPIO 的单板计算机都应该可以工作。
为了方便起见,我订购了一个带有螺钉端子的 GPIO 分线板。
然而,事实证明,这实际上不是那么方便的路线,因为送来的东西是一块板和一套连接器,一点焊锡都没有。
制造商大概进行过如下对话
人员 1:我们应该预先组装好这些吗?
人员 2:这意味着要使用焊锡,这要花钱。
人员 1:但客户会期望它能工作,而且他们可能有一个想要着手的项目。
人员 2:不,这些人是爱好者,他们喜欢焊接。我们将给他们更多他们喜欢的东西,他们会因此而爱我们的
所以,我咕哝着拿起我的“方便”板,并将连接器焊接到上面。这进展得特别不好,因为我自己也进行了一次对话
我:我的好焊枪在哪里?
我:瓦斯动力的那个也得用上
我:哦,它只有一个粗头…总比没有好
我:上次是谁****没订购替换焊锡,当时我几乎用完了?*
事实证明(没有人感到惊讶),当您不得不与笨重的焊枪竞争时,精细焊接可能有点挑战,焊枪正在加热一个又粗又大的头并且将热废气吹到试图抓住不必要地短的焊锡条的手指上。
这可能是我做过的最糟糕的焊接,但我确实焊好了
(不,我不是在自嘲,你还没看过另一面)
拿着我新焊接的分线板,是时候连接传感器了。
首先是温度探针,接线如下
- VCC(红色)到引脚 1(3.3v 电源)
- 信号(黄色)到引脚 7(GPIO 4)
- 接地(黑色)到引脚 6(GND)
- 引脚 1 和 7 之间接 4.7k 电阻
接下来是水流量计
- VCC(红色)到引脚 17(3.3v 电源)
- 信号(黄色)到引脚 29(GPIO 5)
- 接地(黑色)到引脚 39(GND)
- 引脚 17 和 29 之间接 4.7k 电阻
硬件连接好后,我开始着手编写一些代码来收集和提交读数。
收集读数
这两个传感器以不同的方式访问。我可以构建一个更优雅的实现,使用单个脚本来收集两个传感器的读数,但我选择不这样做——部分原因是我想以完全不同的时间间隔进行轮询。
下面提到的脚本的完整副本可在 GitHub 上找到。
温度传感器
温度传感器呈现一个 1-Wire 总线设备,可以直接从中读取
$ cat /sys/bus/w1/devices/28-3c6df6482d24/w1_slave
5a 01 55 05 7f a5 a5 66 c3 : crc=c3 YES
5a 01 55 05 7f a5 a5 66 c3 t=21625
在上面的输出中,传感器报告的温度为 21.63 摄氏度。
28-3c6df6482d24
是探针中芯片的序列号(事后看来,它也告诉我们,很有可能 该芯片是仿冒品)。
因为可以使用 cat
读取探针,所以收集和提交读数非常简单
#!/bin/bash
#
# Read temperature from a 1-wire temperature sensor
#
# example: ./read_temperature.sh "dev name"
#
# Copyright (c) 2023 B Tasker
# Released under BSD 3-Clause
# https://www.bentasker.co.uk/pages/licenses/bsd-3-clause.html
#
DEVNAME=${1:-"28-3c6df6482d24"}
LOCATION=${LOCATION:-"diningroom"}
# Read the sensor
ts=`date +'%s'`
tempread=`cat /sys/bus/w1/devices/$DEVNAME/w1_slave`
temp=`echo "scale=2; "\`echo ${tempread##*=}\`" / 1000" | bc`
# Build line protocol
lp="aquarium,location=$LOCATION water_temperature=$temp $ts"
# Write the stats out
curl -s -d "$lp" "http://127.0.0.1:8086/write?db=telegraf&precision=s"
我将脚本复制到 Pi 并添加了一个 crontab 条目,使其每分钟调用一次。
* * * * * pi /home/pi/tank-monitoring-scripts/app/read_temperature.sh
水流量计
水流量计需要稍微多一点的努力,因为操作系统没有帮助我们公开一个 cat
的接口。
传感器本身非常简单,由涡轮机(由流过的水转动)和霍尔效应传感器(用于拾取涡轮机的旋转)组成。
当涡轮机旋转时,传感器会向 GPIO 引脚发送脉冲,因此我们需要计算这些脉冲,然后将其转换为流速(例如,升/小时)。
最简单的计数可能如下所示
#!/usr/bin/env python3
#
# Copyright (c) 2023 B Tasker
# Released under BSD 3-Clause
# https://www.bentasker.co.uk/pages/licenses/bsd-3-clause.html
#
import RPi.GPIO as GPIO
def countPulse(channel):
''' Callback function for the GPIO event
Increment the counter, assuming the counter is active
'''
global counter
if start_counter == 1:
counter += 1
global counter
counter = 0
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_NUM, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.add_event_detect(GPIO_NUM, GPIO.FALLING, callback=countPulse)
显然,我们需要一些东西来读取 counter
的值。但是,它还需要将计数转换为流速。
传感器上的标签告诉我们如何执行该转换
注释 F=7.5*Q(L/min)
表示每 7.5 个脉冲相当于 1 升/分钟的流速。
这意味着我们需要执行的数学运算非常简单:计数一秒钟的脉冲,然后将计数除以 7.5
以获得升/分钟的流速。
# We use this list to track rates between writes
flowrates = []
while True:
try:
# Capture some pulses
start_counter = 1
time.sleep(1)
start_counter = 0
# Calculate the flow rate
# Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min.
flow = (counter / 7.5)
# Add to the list of observed rates
flowrates.append(flow)
counter = 0
time.sleep(5)
每次迭代都会计算流速,并将其推送到列表 flowrates
中。
5 秒的读取频率比我真正需要提交的频率要高得多,但我想定期读取这些频率,并在写入 InfluxDB 之前定期聚合它们
# We use this list to track rates between writes
flowrates = []
while True:
try:
# Capture some pulses
start_counter = 1
time.sleep(1)
start_counter = 0
# Calculate the flow rate
flow = (counter / 7.5) # Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min.
# Add to the list of rates
flowrates.append(flow)
# Write out
if len(flowrates) >= 4:
# Calculate bounds and average
stats = {
"min" : min(flowrates),
"max" : max(flowrates),
"mean" : sum(flowrates) / len(flowrates)
}
writeStat(stats, session)
flowrates = []
counter = 0
time.sleep(5)
现在,脚本定期写出该采样间隔的平均值、最小值和最大值流速。
在整理了一下脚本之后,我准备好部署它了,因此将其复制到 Pi 并创建了一个单元文件
[Unit]
Description=FlowRate Monitor
After=multi-user.target
[Service]
Type=simple
ExecStart=/home/pi/tank-monitoring-scripts/app/read_flowrate.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
然后我安装、启用并启动了它。
sudo -s
cp flowrate.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable flowrate
systemctl start flowrate
使用 Telegraf 收集
虽然这两个脚本可以直接写入我的 InfluxDB 实例,但这两个脚本都没有实现写入缓冲区,因此如果我的 Influx 实例不可用(无论是由于它已关闭还是由于鱼缸 Pi 出现 Wifi 问题),这些写入将丢失。可能不是世界末日,但也远非理想。
向 Python 脚本添加缓冲区很容易,InfluxDB Python 客户端库 支持批量写入,但是将其添加到 BASH 脚本中就不会那么简单了。在任何一种情况下,如果脚本必须重新启动,缓冲的写入将丢失——考虑到我为编写它们所付出的努力确实很少,这并不理想。
因此,我决定让脚本改为写入 Telegraf 的 InfluxDB_Listener 输入插件,以便它可以针对下游故障提供缓冲区。作为额外的积极好处,它还允许收集系统指标,以帮助监控 Pi 本身。
我正在 Docker 中运行 Telegraf
(因为我最初打算 Docker 化收集脚本,但后来决定它们太简单了,真的不值得这样做)。
Telegraf 是从以下 docker-compose.yml
启动的
version: '3.1'
services:
telegraf:
image: telegraf
restart: always
user: telegraf:995
container_name: telegraf
network_mode: "host"
environment:
HOST_ETC: /hostfs/etc
HOST_PROC: /hostfs/proc
HOST_SYS: /hostfs/sys
HOST_VAR: /hostfs/var
HOST_RUN: /hostfs/run
HOST_MOUNT_PREFIX: /hostfs
ports:
- 8086:8086
volumes:
- /home/pi/tank-monitoring-scripts/config/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf
- /var/run/docker.sock:/var/run/docker.sock
- /:/hostfs:ro
Telegraf 的配置文件包含以下内容
[agent]
interval = "1m"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
flush_interval = "10s"
flush_jitter = "0s"
debug = false
quiet = true
hostname = "tankmonitor"
omit_hostname = false
[[inputs.cpu]]
[[inputs.diskio]]
[[inputs.mem]]
[[inputs.net]]
[[inputs.processes]]
[[inputs.swap]]
[[inputs.system]]
[[inputs.disk]]
ignore_fs = ["tmpfs", "devtmpfs", "devfs", "overlay", "aufs", "squashfs"]
# Add the listener
[[inputs.influxdb_listener]]
service_address = "127.0.0.1:8086"
# Send it all to the local InfluxDB instance
[[outputs.influxdb]]
urls = ["http://192.168.3.84:8086"]
database = "telegraf"
启动 Telegraf 非常简单,只需
docker-compose up -d
然后,监控脚本可以简单地将行协议写入 http://127.0.0.1:8086/write
,就像它们正在与 InfluxDB 1.x 实例对话一样。
硬件安装
传感器启动并运行后,是时候将所有设备连接到鱼缸了。
水流量计需要连接到过滤器回路中。它需要安装在回流管道中,而不是供水管道中:不仅它可能会被未经过滤的污垢堵塞,而且我们还想知道水是否真的回流到鱼缸中,而不是(例如)从罐体中泄漏出来。
流量计具有 G1/2 螺纹,因此我使用了一对 16 毫米倒钩到 G1/2 的连接器。
我在这里遇到了几个小问题
- 连接器到货时没有垫圈。
- 我的过滤器管道虽然外部尺寸为 20 毫米,但内径为 15 毫米(不是更标准的 16 毫米)。
第一个问题要么需要耐心(订购一些 O 形圈),要么需要密封剂(我手头有),第二个问题只需要一点肘部润滑脂。
虽然看起来连接良好且防水,但它仍然是一个接头,因此在学会信任它之前(或者至少在我下次需要水桶之前)它将住在水桶中。
接下来是安装 Pi 本身:我将它的外壳底座拧到鱼缸柜子上,确保它位于悬垂下方(以帮助防止意外滴水)。
最后,需要将温度传感器安装到位。我使用扎带(类似于您可能在冷冻袋上使用的那种)将其连接到吸盘钩上,然后将其安装在鱼缸中。
此后,我订购了一些带有夹子的吸盘,旨在承载水族箱空气管路——夹子的直径非常适合传感器,并且看起来比扎带整洁得多。
一切都连接并就位后,我插入 Pi 的电源,在一分钟内,读数开始到达 InfluxDB。
绘图
随着读数进入数据库,接下来要做的是在 Grafana 中构建仪表板,以便可视化该数据
右上角的空白区域将在探针到达后包含 PH 值信息。
底层查询没有什么特别之处,顶部的状态指示器只是查询功耗并评估它是否超过阈值。
from(bucket: "Systemstats/autogen")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "power_watts")
|> filter(fn: (r) => r._field == "consumption")
|> filter(fn: (r) => r.host == "aquarium-filter-pump")
|> aggregateWindow(every: v.windowPeriod, fn: last)
|> map(fn: (r) => ({Device: r.host,
_state: if r._value > 9 then
1
else
0
}))
泵在活动状态下仅使用 10 瓦功率,因此我们检查它是否消耗超过 9 瓦功率以确认它是否已实际开启。
流速指示器只是提取一段时间内的平均值
from(bucket: "telegraf")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "aquarium")
|> filter(fn: (r) => r._field == "flow_mean")
|> aggregateWindow(every: v.windowPeriod, fn: last)
页面下方还有一个图表,显示了流速范围随时间的变化情况。
它们之间差异的相对一致性可能反映了采样问题,而不是泵速的真实波动。但是,我们真正关心的是水是否以相对一致的速率泵送。
警报
我想尝试使用 Grafana 的内置警报功能,因此在将 SMTP 详细信息添加到 grafana.ini
后,我将自己添加为 联系点,然后继续创建一个简单的警报。
用于警报的查询是
from(bucket: "telegraf")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "aquarium")
|> filter(fn: (r) => r._field == "water_temperature")
|> mean()
警报只是计算过去 5 分钟内报告的平均水温,如果水温不在 22 到 26 摄氏度之间(许多热带鱼实际上可以容忍 20-28 摄氏度,但这些更严格的阈值允许在鱼受到太大压力之前做出响应)。
当警报触发时,会发送电子邮件。
如果接收到的数据中断(例如,因为有人不小心购买了仿冒的温度探针,导致探针脱离总线…),则会发出死信警报邮件。
当然,我们想要发出警报的不仅仅是温度:因为我们收集过滤器流速,所以如果流速降至阈值以下,我们可以发出警报(表明泵出现问题或过滤器或其入口堵塞)。
我还有一个单独的警报,带有上限阈值——如果鱼缸突然开始非常快速地抽水,则可能表明我们正在将水泵送到仪表以外的某个地方的地板上。
还有基于功耗的警报,用于检查计划任务(如每日表面撇渣器运行)是否实际发生。这些警报由(或多或少)与仪表板中的状态单元格相同的查询驱动。
一旦我有了所需的组件,我也会创建一个基于 PH 值的警报。
结论
自动化收集水族箱的指标非常简单。
所涉及的大多数组件也相对便宜,最昂贵的传感器是水流量计(10 英镑)。显然,如果我没有现成的 Raspberry Pi,该项目的成本会更高,尽管应该可以使用廉价的 Raspberry Pi Zero 或 Arduino Nano 来完成所有这些工作。
与任何涉及水和管道的设备一样,安装水流量计存在一些风险:任何接头都存在泄漏的风险。如果您真的不介意外观,可以通过将流量计安装在鱼缸内部(显然在水位线以上)来减轻这种风险,这样任何泄漏都将留在鱼缸内部。
既然我已经设置了基础系统,那么不仅即将推出的 PH 传感器可以安装,市场上还有各种其他探针可以帮助监控水质——例如,总溶解固体 (TDS) 传感器可能可以用于预测水柱中存在的硝酸铵水平。
正如我在温度传感器中发现的那样,真正的挑战在于找到真正、可靠且价格合理的传感器硬件。
除了提供自动化监控/警报外,设置所有这些还有助于在等待鱼缸循环时消磨一点时间。