使用 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的温度探头(一个单总线设备)
- 一个Digitem FL-408水流量计
显然,未来的PH监控项目也将需要一个自己的物理探头。
关于温度传感器的一些说明
如果您计划构建类似的东西,请注意,从可靠的供应商(如RS Components)购买您的温度探头非常重要。市面上有许多假的DS18B20芯片,它们往往会以令人烦恼且不可预测的方式表现。
上图所示的温度传感器是从亚马逊购买的,最初似乎工作正常。然而,大约6小时后,它养成了从总线上断开连接的习惯,除非Pi完全重新启动,否则不会重新出现。
这种表现,显然,在低劣的芯片中并不少见。它们还可能返回一些非常可疑的读数。
没错,如果相信探头的话,那么(在片刻之间)我的鱼缸水温从约20摄氏度飙升到3倍的水临界温度...
一旦我意识到这个问题,我就从DSRobot订购了一个替换探头。它甚至比原装的要便宜:仿制品不仅存在于价格范围的低端。
树莓派搭建
我有一个未使用的树莓派4,因为它的SD卡损坏(当时我将东西迁移到其他地方),所以我决定使用它。从理论上讲,任何带GPIO的单板计算机都应适用。
为了方便起见,我订购了一个带螺丝端子的GPIO扩展板。
然而,实际上这并不是那么方便,因为出现的只是一个板子和一套连接器,上面没有一滴焊锡。
制造商可能有过以下这样的对话
第一个人:我们应该提供这些预先组装的板子吗?
第二个人:那就需要用到焊锡,这会花费成本。
第一个人:客户会期望它能够工作,他们可能有一个项目想要开始。
第二个人:不,这些人都是爱好者,他们热爱焊接。我们会给他们更多他们喜欢的东西,他们会因此喜欢我们。
因此,在抱怨中,我拿起了我的“方便”板,并将连接器焊接在上面。这并没有特别顺利,因为我有过自己的对话
我:我的好焊锡铁在哪里?
我:气焊锡铁将就着用。
我:哦,它只有一个大尖端…总比没有好。
我:哪**个**人上次快焊完的时候没有订购替换焊锡?*
结果(不出所料)是,在不得不与一个不灵活的焊锡铁竞争的情况下,它正在加热一个又大又胖的尖端,并且向试图抓住不必要的短焊锡的手指吹出热气,进行精细的焊接确实是一项挑战。
这可能是我做过的最糟糕的焊接,但我还是把它焊上了。
(不,我并没有在自贬,你没有看到另一面)
手里拿着新焊接好的扩展板,是时候连接传感器了。
首先连接温度探头,接线如下
- 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线总线设备,可以简单地从中读取
$ 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次闪烁相当于1L/min的速率。
这意味着我们需要进行的数学运算非常简单:对脉冲计数一秒,然后除以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螺钉螺纹,所以我使用了一对16mm的G1/2连接器。
我在这里遇到了几个小问题
- 连接器到达时没有垫圈。
- 我的过滤器的软管,尽管外径为20mm,内径为15mm(而不是更标准的16mm)。
第一个问题是耐心(订购一些O型环)或密封剂(我手头有),第二个问题只需要一点力气。
尽管看起来连接得很好,并且是防水的,但它仍然是一个接头,所以它将暂时放在桶里,直到我学会信任它(或者至少,直到我下次需要桶)。
接下来是安装Pi本身:我将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内置的警报功能,因此在我的grafana.ini
文件中添加SMTP详细信息后,我把自己添加为一个联系点,然后继续创建一个简单的警报。
警报所使用的查询是
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英镑)。显然,如果我没有手头已有的树莓派,这个项目的成本会更高,尽管使用便宜的树莓派Zero或Arduino Nano也应该能够完成所有这些。
就像任何涉及水和管道的东西一样,安装水流量计有一些风险:任何接口都存在泄漏的风险。如果您真的不在乎外观,可以通过将仪表安装在鱼缸内部(显然在水线以上)来减轻风险,这样任何泄漏都会保持在鱼缸内部。
现在我已经设置了基本系统,不仅仅是即将安装的PH传感器,市场上还有各种其他探头可以帮助监测水质——例如,总溶解固体(TDS)传感器可能被用来预测水柱中存在的铵硝酸盐的水平。
真正的挑战,就像我发现我的温度传感器那样,是在找到真实、可靠、价格合理的传感器硬件。
除了提供自动监控/警报外,设置这一切还有助于在等待鱼缸循环时消磨时间。