使用 InfluxDB 和 Grafana 监控水族箱

导航至

这篇文章最初发布在 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 作为计时器来安排表面撇沫器和鱼缸灯光等设备的运行。

actions

仅观察电力消耗是不够的,因为我们关心的某些指标需要采取 物理 测量。

例如,观察泵的能耗只是解决方案的一部分:如果叶轮发生故障,电机将继续无用地消耗电能。水族过滤器有许多种故障模式,电机故障只是其中之一,我们真正关心的是泵所达到的流量

因此,我们需要一些传感器

probe and meter

所使用的传感器有

  • 基于DS18B20的温度探头(一个单总线设备)
  • 一个Digitem FL-408水流量计

显然,未来的PH监控项目也将需要一个自己的物理探头。

关于温度传感器的一些说明

如果您计划构建类似的东西,请注意,从可靠的供应商(如RS Components)购买您的温度探头非常重要。市面上有许多假的DS18B20芯片,它们往往会以令人烦恼且不可预测的方式表现。

上图所示的温度传感器是从亚马逊购买的,最初似乎工作正常。然而,大约6小时后,它养成了从总线上断开连接的习惯,除非Pi完全重新启动,否则不会重新出现。

这种表现,显然,在低劣的芯片中并不少见。它们还可能返回一些非常可疑的读数。

mental temperature

没错,如果相信探头的话,那么(在片刻之间)我的鱼缸水温从约20摄氏度飙升到3倍的水临界温度...

一旦我意识到这个问题,我就从DSRobot订购了一个替换探头。它甚至比原装的要便宜:仿制品不仅存在于价格范围的低端。

树莓派搭建

我有一个未使用的树莓派4,因为它的SD卡损坏(当时我将东西迁移到其他地方),所以我决定使用它。从理论上讲,任何带GPIO的单板计算机都应适用。

为了方便起见,我订购了一个带螺丝端子的GPIO扩展板。

gpio breakout amz small

然而,实际上这并不是那么方便,因为出现的只是一个板子和一套连接器,上面没有一滴焊锡。

制造商可能有过以下这样的对话

第一个人:我们应该提供这些预先组装的板子吗?

第二个人:那就需要用到焊锡,这会花费成本。

第一个人:客户会期望它能够工作,他们可能有一个项目想要开始。

第二个人:不,这些人都是爱好者,他们热爱焊接。我们会给他们更多他们喜欢的东西,他们会因此喜欢我们。

因此,在抱怨中,我拿起了我的“方便”板,并将连接器焊接在上面。这并没有特别顺利,因为我有过自己的对话

我:我的好焊锡铁在哪里?

我:气焊锡铁将就着用。

我:哦,它只有一个大尖端…总比没有好。

我:哪**个**人上次快焊完的时候没有订购替换焊锡?*

结果(不出所料)是,在不得不与一个不灵活的焊锡铁竞争的情况下,它正在加热一个又大又胖的尖端,并且向试图抓住不必要的短焊锡的手指吹出热气,进行精细的焊接确实是一项挑战。

这可能是我做过的最糟糕的焊接,但我还是把它焊上了。

GPIO breakout

(不,我并没有在自贬,你没有看到另一面)

手里拿着新焊接好的扩展板,是时候连接传感器了。

首先连接温度探头,接线如下

  • VCC(红色)连接到引脚1(3.3V电源)
  • 信号(黄色)连接到引脚7(GPIO 4)
  • 接地(黑色)连接到引脚6(GND)
  • 在引脚1和7之间接一个4.7k欧姆的电阻

wired temperature probe

接下来是水流计

  • VCC(红色)连接到引脚17(3.3V电源)
  • 信号(黄色)连接到引脚29(GPIO 5)
  • 接地(黑色)连接到引脚39(GND)
  • 在引脚17和29之间接一个4.7k欧姆的电阻

wired water meter

硬件连接完成后,我开始编写代码以收集和提交读数。

收集读数

这两个传感器可以通过不同的方式访问。我可以构建一个更优雅的实现,使用单个脚本收集两个传感器的读数,但选择了不这样做——部分原因是因为我想要在不同的时间间隔进行轮询。

下面提到的脚本的完整副本可在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的值。但是,它还需要将计数转换为流速。

传感器上的标签告诉我们如何进行转换

probe label

注释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连接器。

g12 barb small

我在这里遇到了几个小问题

  • 连接器到达时没有垫圈。
  • 我的过滤器的软管,尽管外径为20mm,内径为15mm(而不是更标准的16mm)。

第一个问题是耐心(订购一些O型环)或密封剂(我手头有),第二个问题只需要一点力气。

water flow sensor

尽管看起来连接得很好,并且是防水的,但它仍然是一个接头,所以它将暂时放在桶里,直到我学会信任它(或者至少,直到我下次需要桶)。

接下来是安装Pi本身:我将Pi机箱的底部固定在罐子的柜子上,确保其位于一个悬挑物下方(以帮助防止意外的滴水)。

pi installed

最后,需要放置温度传感器。我使用一个绳结(就像你可能用来装冷冻袋的那种)将其固定在吸盘钩上,然后将其安装在罐子中。

temperature sensor

我后来订购了一些带夹子的吸盘,它们旨在携带水族馆空气线——夹子的直径正好适合传感器,看起来比绳结整洁得多。

一切连接并就绪后,我插上Pi的电源,不到一分钟,读数就开始出现在InfluxDB中。

绘图

随着数据进入数据库,接下来要做的事情是在Grafana中构建一个仪表板,以便可视化这些数据

grafana dashboard

右上角的间隙将在探头到达后包含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)

页面下方还有一个图表,显示了流量界限随时间的变化。

flow rates

它们之间差异的相对一致性可能反映的是采样问题,而不是泵速率的真实波动。然而,我们真正关心的是,水泵的抽水速率相对稳定。

警报

我想尝试使用Grafana内置的警报功能,因此在我的grafana.ini文件中添加SMTP详细信息后,我把自己添加为一个联系点,然后继续创建一个简单的警报。

grafana alert config

警报所使用的查询是

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摄氏度,但这些更紧的阈值允许有时间在鱼太紧张之前做出反应)。

当警报触发时,会发送一封电子邮件。

grafana alert mail

如果有数据接收中断(例如,由于有人不小心购买了一个掉落的假冒温度探头…)将发送一封警报邮件通知。

grafana deadman

当然,我们希望警报的不仅仅是温度:因为我们收集了过滤流量,如果它低于阈值(这表明泵的问题或过滤器的堵塞或其入口处的堵塞),我们可以发出警报。

flow rate alert

我还设置了一个上限警报——如果鱼缸突然开始非常快速地抽水,这可能表明我们在某个地方将水抽得满地都是。

还有一些基于功耗的警报,用于检查计划任务(如每日表面撇沫器运行)是否真的发生了。这些是由(或多或少)与仪表板中的状态单元格所使用的相同查询驱动的。

grafana scheduled activity alerts

一旦我有了相应的组件,我还会创建一个基于PH水平的警报。

结论

自动收集水族箱的指标相当简单。

涉及的组件也相对便宜,最贵的传感器是水流量计(10英镑)。显然,如果我没有手头已有的树莓派,这个项目的成本会更高,尽管使用便宜的树莓派ZeroArduino Nano也应该能够完成所有这些。

就像任何涉及水和管道的东西一样,安装水流量计有一些风险:任何接口都存在泄漏的风险。如果您真的不在乎外观,可以通过将仪表安装在鱼缸内部(显然在水线以上)来减轻风险,这样任何泄漏都会保持在鱼缸内部。

现在我已经设置了基本系统,不仅仅是即将安装的PH传感器,市场上还有各种其他探头可以帮助监测水质——例如,总溶解固体(TDS)传感器可能被用来预测水柱中存在的铵硝酸盐的水平。

真正的挑战,就像我发现我的温度传感器那样,是在找到真实、可靠、价格合理的传感器硬件。

除了提供自动监控/警报外,设置这一切还有助于在等待鱼缸循环时消磨时间。