USB-RH + ZABBIXで室温・湿度のモニタリングを行う


 少し前に、息子に金魚を買ってあげようと思ってペットショップに行ったのだが、そこで格安で売られているチワワを見つけてしまい、ついつい買ってしまった。当時生後2ヶ月半前後で、特に障害もない。むしろチワワとしてもかなり可愛い部類に入っていて、どう見ても15万前後するような犬が Mac mini を買うような感覚で買えるお値段だったのでお救いするしかないと思った次第。

チワワ

 結局我が家ではリビングに犬用サークルを置いてそこで飼っているが、深夜帯は1匹そこで寝ていることもあって、室温の相当な低下が予想される。だが、当然寝ているのでどれくらいの室温になっているか全く不明なので、Strawberry Linux から出ている USB-RH を設置して、Gentoo Linux と ZABBIX を組み合わせてモニタリングすることにした。

USB-RH

 USB-RH は、Sensirion から出ている温度・湿度センサ SHT11 を搭載していて、25℃での誤差は温度が ±0.4℃、相対湿度が ±3.0%RH となる。SHT11 のデータシートを見ると 25℃から離れるにつれ誤差が大きくなるようだが、

SHT11の誤差

それでも犬の環境に問題がないかのモニタリングには十分だろう。

 さて、USB-RH からデータを読み出す方法だが、先達がカーネルドライバを書いておられるので、それを利用するのが手っ取り早い。これだと複数の USB-RH に対応しているし、データも単に読み出すだけで取得できるので便利だと思われる。が、Gentoo Linux で運用しているという都合上、ちょくちょくカーネルを更新する可能性があること、そして何よりも自分で読みだしてみたいので /dev/usbhid* から読み出すコードを書いてみた。

 USB-GEIGER も USB-RH も同じ CY8C24794 で制御されていて、ドライバがコンパイルされていれば HID デバイスとして認識される。だが、最近の Linux Box はマウスやキーボードなども USB 接続されていたりして、デバイスファイルが常に固定されているかといえばそういう訳ではない。ログにはデバイスファイルが書き出されているが、調べるのも面倒なので udev を使ってデバイスファイルを調べるようにしてみた。Google 先生曰く、HID デバイスを libudev を使って検索する方法は libudev and Sysfs Tutorial を参考にできる。ここに HID デバイスを列挙するサンプルコードがあるので、これを修正してベンダ ID = 1774、プロダクト ID = 1001 のデバイスを検索すれば良い。

const char *probe_usb_rh (const char vendor_id, const char product_id)
{
  struct udev            *udev;
  struct udev_enumerate  *enumerate;
  struct udev_list_entry *devices,
                         *dev_list_entry;
  struct udev_device     *dev;
  const char *node;

  udev = udev_new();
  if (!udev) {
    return NULL;
  }
  enumerate = udev_enumerate_new(udev);
  udev_enumerate_add_match_subsystem(enumerate, "hidraw");
  udev_enumerate_scan_devices(enumerate);
  devices = udev_enumerate_get_list_entry(enumerate);

  udev_list_entry_foreach(dev_list_entry, devices) {
    const char *path;

    path = udev_list_entry_get_name(dev_list_entry);
    dev = udev_device_new_from_syspath(udev, path);

    node = udev_device_get_devnode(dev);

    dev = udev_device_get_parent_with_subsystem_devtype(
                                                        dev,
                                                        "usb",
                                                        "usb_device");
    if (!dev) {
      udev_device_unref(dev);
      udev_enumerate_unref(enumerate);
      udev_unref(udev);
      return NULL;
    }

    if (strcmp(vendor_id,  udev_device_get_sysattr_value(dev, "idVendor"))  == 0 &&
        strcmp(product_id, udev_device_get_sysattr_value(dev, "idProduct")) == 0) {
      udev_device_unref(dev);
      udev_enumerate_unref(enumerate);
      udev_unref(udev);
      return node;
    }
  }

  udev_enumerate_unref(enumerate);
  udev_unref(udev);
}

これで、probe_usb_rh("1774", "1001") でデバイスファイルのパスが返ってくる。接続されていなければ NULL ポインタが返るので、呼び出し元でチェックすれば ok。

 次にデータ読み出しだが、デバイスファイルからデータを読み出す件については、同系統の USB-GEIGER での事例がある (うーたまっくす – USB-GEIGER を Linux で動かす) のでありがたく参考にさせていただく。読み書きするのも 8 バイト、読み出し前に 0 クリアしたバッファを一旦書き出してから読み出すのも USB-GEIGER と全く共通になる。

int usb_rh_open (const char *node)
{
  int fd;

  if ((fd = open(node, O_RDWR)) < 0) {
    return 0;
  }
  flock(fd, LOCK_EX);

  return fd;
}

void usb_rh_close (int fd)
{
  close(fd);
}

int usb_rh_send_command (int fd)
{
  unsigned char msg[8];
  int result;

  memset(msg, 0x00, sizeof(msg));
  if (write(fd, msg, sizeof(msg)) != sizeof(msg)) {
    return 0;
  }
  return 1;
}

int usb_rh_get_values (int fd, unsigned char *buf)
{
  fd_set fds;
  struct timeval timeout;
  int result;

  if (usb_rh_send_command(fd)) {
    timeout.tv_usec = 0;
    timeout.tv_sec  = 1;
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    switch (select(fd + 1, &fds, NULL, NULL, &timeout)) {
    case 0:
      return 0;
    default:
      ;
    }
    if (read(fd,buf,sizeof(buf)) < 0) {
      return 0;
    }

    return 1;
  }
}

 実データは 4 バイトからなり、最初 2 バイトが湿度、次の 2 バイトが温度になる。ただし、USB-GEIGER と違ってビッグエンディアンなので気を付ける必要はある。

 後はデータシートに従って温度と湿度を計算すれば良い。まず、温度を計算する。取得した温度を rawtemp とすると、

temperature = -40.0 + 0.01 rawtemp

となる。次に、取得した湿度 rawhumidity から温度補償のない湿度 linearhumidity を計算する。これは二次関数で、

linearhumidity = -4 + 0.0405 rawhumidity – 0.0000028 rawhumidity ^ 2

で表される。これに対して温度補償を加えると

humidity = (temperature – 25) (0.01 + 0.00008 rawhumidity) + linearhumidity

これが湿度となる。これをデータとして書き出せばよい。うちの場合はデーモンとして 5 秒おきに JSON で UNIX タイムスタンプと共に書きだすようにした。

{
  "status": "ok",
  "epoch": 1328523978,
  "results": {
    "temperature": 25.49,
    "humidity": 38.82
  }
}

 ZABBIX エージェントがデータを読み出すたびに計測してもいいのだが、SHT11 の場合は最高の精度で読み出すには 1 秒ほどかかってしまい、ZABBIX エージェントがタイムアウトしてしまう。さらに連続稼働させるとセンサの発熱で誤差が大きくなるので、定期的に読み出してファイルに書き出しておき、それを ZABBIX から読み出すほうが何かと良さそうだった。ただ、センサからの読み出しと ZABBIX への投入を別プロセスとすると、途中のファイルで競合が発生し、1 分ごとの更新だと1日に 2 〜 3 回、データが正しく取れないタイミングが発生する。そのため、データの読み書きの際には flock しておくとよい。

 最後に、ZABBIX で読み出せるようにする。ZABBIX で読み出すには単純にデータを標準出力に書き出すスクリプトを書けばよい。ただ、うちの場合はデータ読み出しが別プロセスなので、JSON に含まれるタイムスタンプが閾値より古いか status が ok ではない (= USB-RH にアクセスできない) 場合は湿度を 0 として書き出すようにした。日本で湿度が 0%RH となるケースはほとんどなく、1971 年 1 月 19 日に鹿児島県屋久島で一度だけ観測された程度らしい。なので、湿度が 0 かどうかのトリガをセットしておいて、データ取得が止まっているかを ZABBIX でチェックするようにする。

#!/usr/bin/ruby -Ku
# -*- coding: utf-8 -*-

require 'json'
require 'time'

data = {}
begin
  File.open('/path/to/data.json') do |f|
    f.flock(File::LOCK_EX)
    data = JSON.parse(f.read)
  end
rescue
  puts "0"
  exit
end

if (Time.now.to_i - data['epoch'] > 60)
  puts "0"
elsif (data['status'] == 'ok' && !data['results'].nil?)
  mode = ARGV[0] == 'humidity' ? 'humidity' : 'temperature'
  puts data['results'][mode]
else
  puts "0"
end

 ZABBIX でデータ取得するには、/etc/zabbix/zabbix_agentd.conf でスクリプトを実行するようにしてエージェントを再起動する。

UserParameter=env.temperature,/usr/local/bin/usbrh.rb temperature
UserParameter=env.humidity,/usr/local/bin/usbrh.rb humidity

 あとは ZABBIX 側でアイテムを追加すればよい。今回、湿度は小数点も含むので、データ型は float にしておく必要がある。

ZABBIXでの湿度監視設定
ZABBIXでの湿度監視設定

 アイテムを追加すれば、そのアイテム単体についてグラフを確認できるようになる。

ZABBIX での温度推移グラフ
ZABBIX での温度推移グラフ

ZABBIX での湿度推移グラフ
ZABBIX での湿度推移グラフ

 また、せっかく ZABBIX を使っているのだからデータ取得に成功しているかを監視しておくといいわけだが、前述の通り、湿度 = 0 はデータ取得に失敗している状態になる。なので、それを踏まえてトリガも追加しておく。

{ホスト名:env.humidity.last(0)}=0

 これで ZABBIX を使った室温・湿度の監視が可能になる。室温が一定の閾値を超えたらアラートメールを送信したりもできるので、チワワの生活環境の維持には重要な情報になるものと思われる。