虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

水槽の水替え時期を教えてくれる仕組みを作った。

こんにちは、虎の穴ラボのH.Y.です。

本記事は虎の穴ラボ Advent Calendar 2022の20日目の記事になります。

昨日19日目はT.K.さんによる「とらラボ雑談タイム「リモートコミュニケーション」の現在」が投稿されています。
明日21日目は辻村さんによる「RailsでMariaDBのCHECK制約を試行してみた話」が投稿される予定です。

こちらもぜひご覧ください。

最近趣味で作った機能を紹介したいと思います。

きっかけ

在宅勤務が始まってから、アクアリウムにハマり始めました。
ハマり出すと水槽の数が増えていき、現在では5個にまで増えています。
それに伴い、個々の水槽ごとのメンテナンスサイクルが把握できなくなってきました。

それで、各水槽のメンテナンスサイクルを教えてくれる仕組みを作ればいいじゃないかと思いました。

アクアリウムでのメンテナンスについて

アクアリウムに必要な定期的に行うメンテナンスは、主に以下の3つだと考えました。
・水槽の苔掃除
・フィルタ掃除
・水替え

水槽の苔掃除

水槽に茶色や緑色などの苔が生えてきます。
この苔が見た目上良くないので、苔を取り除くメンテナンスになります。
逆に言えば、見た目だけなので、自分が掃除したいと思うタイミングで行うため、
あえて機能化する必要がありません。

フィルタ掃除

フィルターとは、餌の残り、糞などのを物理的なゴミを取り除く物理濾過と
バクテリアなどが科学的に有害物質をやや無害の物質に変える生物濾過があります。
物理濾過は時間が経つとゴミが蓄積して目詰まりを起こすので、これを解消するメンテナンスが必要です。
ただ、2ヶ月ごとのように、決まった期間でメンテナンスするので、スケジューラー的なアプリを使えばいいので、
機能化の必要はありません。

水替え

フィルターの生物濾過の働きで、有害な「アンモニア」→やや有害な「亜硝酸塩」→やや無害な「硝酸塩」と変換されていきます。
硝酸塩は脱窒菌によって窒素に分解されたり、水草が吸収してくれたりしますが、発生スピードの方が速いため硝酸塩が溜まります。
この溜まった硝酸塩など有害な物質をリセットするのが水替えというメンテナンスです。


この硝酸塩が溜まるのは、生体(魚など)と餌、気温などで、スピードが変動するため、この水替えというメンテナンスの時期を知らせる仕組みを作ります。

機能概要

水替え指標の数値化

まず、機能化するにあたって、水替えする要素を数値化する必要があります。
硝酸塩濃度を直接取得するのが最も良い手段ですが、センサーが高いという問題があります。

しかし、硝酸塩を測るにはTDSという指標があります。


TDSとは総溶解固形物(硝酸塩、亜硝酸塩、水道水のミネラルなどの無機物、有機物)の全ての濃度になります。
十分にバクテリアが定着したフィルターを使用している場合は、水道水-飼育水≒硝酸塩になるので
硝酸塩濃度を実質数値化できるようになります。

TDSの濃度測定は、2000円ぐらいのセンサーで測定できるので、安価に実現できます。


システム概要

今回のシステムのアーキテクチャ図は以下の通りです。


1日に1、2回ぐらいしか実行しないため、Lambdaを使用します。
ランタイムは使用経験があるPython3.9を使用します。
同様に、回数少ないので、DBはDynamoDBを使用します。
通知方法は、メールとかweb画面などいろいろありますが、
個人的にメールはあんまり見ないので、Slackを使用します。

TDS記録機能

DynamoDBに日付とTDSと水槽IDを記録する機能です。
DynamoDBに記録するだけなので、大した機能はないですが、
以下の3点を工夫しました。

DynamoDBに日付型が存在しない

これは、エポック秒を記録することで対応しました。

入力インターフェイス

Lambdaのソースを直に修正して、テスト実行によりDB書き込みを行います。

水替えするとTDSスピードがマイナスになる時がある

水替え直後の最初のTDS測定は水替えフラグを立てて、計算外になるようにします。

    waterTankId = 2 # 水槽ID
    date = int(time.time()) # 日付
    tds = 93 # TDSの値
    waterChangeFlg = 1 # 水替えフラグ

    table = dynamodb.Table("WaterTankStatusHistory")
    # DBへのinsert
    table.put_item(Item={
        'WaterTankId': waterTankId,
        'Date': date,
        'Tds': tds,
        'WaterChangeFlg': waterChangeFlg})
    


通知機能

Slackへの通知

Incoming Webhookを使用してSlackに通知します。
設定した閾値(100)を超えたらメンションをつけてSlackに通知するようにしました。

TDSの予測機能

DynamoDBからデータを読み取って、水替え直後のデータを除く直近TDSの2点から、TDS上昇スピードを計算し通知します。

  waterTable = dynamodb.Table("WaterTank")
  table = dynamodb.Table("WaterTankStatusHistory")
  
  waterTableResponse = waterTable.scan()
  
  waterTableItems = waterTableResponse["Items"]

  for waterTableItem in waterTableItems:  # waterTableItemsは水槽データが入ったテーブル
    print(waterTableItem)
  
    response = table.query(
      KeyConditionExpression = Key("WaterTankId").eq( waterTableItem["id"] ) , # 取得するKey情報
      ScanIndexForward = True, # 昇順か降順か(デフォルトはTrue=昇順)
      Limit = 1000 # 取得するデータ件数
    )
    
    items = response["Items"]

    itemList = []
    speed = 0.0
    mae = {
        "Date": 0,
        "Tds": 0,
        "WaterTankId": 0,
        "WaterChangeFlg": 0
      }
    last_speed = 0.0
    now_tds = 0.0
    
    for item in items:
      JST = timezone(timedelta(hours=+9), 'JST')
      # datetime(JST)に変換
      dt = datetime.fromtimestamp(item["Date"]).replace(tzinfo=timezone.utc).astimezone(tz=JST)
      
      # TDSスピードの算出
      speed = ((item["Tds"] - mae["Tds"])/ (item["Date"] - mae["Date"])) * 3600 * 24

      itemList.append(
          {
            "Date": dt.isoformat(),
            "Tds": item["Tds"],
            "WaterTankId": item["WaterTankId"],
            "WaterChangeFlg": item["WaterChangeFlg"],
            "DailySpeed": speed
          }
        )

        
      mae =  item
      
      if item["WaterChangeFlg"] == 0:
        last_speed = speed
      
    # TDSの現在値を算出
    now_tds = ((last_speed * (int(time.time()) - mae["Date"])) / 3600 / 24 ) + mae["Tds"]
    
    # Slack通知内容を作成
    msg = "今日のTDS:" + "{:.1f}\n".format(now_tds)
    msg += 'TDS上昇スピード:' + '{:.1f}'.format(last_speed)
    # 閾値を超えたらhereをつける
    if now_tds > 100:
       msg += '\n<!here>'
    print(msg)

    # Slackへの通知
    send_slack(msg, waterTableItem["WebhookUrl"], waterTableItem["slackName"])

トリガー

EventBridgeを利用して、毎日21時に通知するようにします。
ちなみに、21時は、在宅の確率が高いのと、寝るまでに水替えができる作業時間を確保できるちょうどいい時間と思い設定しました。

通知結果

水槽ごとにチャンネルを分けることで、一覧を見るだけで、どの水槽で水替えが必要かがわかるようになっています。

運用してみて(まとめ)

予測精度

しばらく運用してみて、変化が一定の割ということがわかりました。
50→70(予測)68(実測)
もっと複雑な変化なら役に立たないと不安でしたが、実用的な機能ということがわかりました。
ただ、魚が大きくなるにつれてTDS上昇スピードも速くなっている傾向が長期的にあるので、
この機能は有用だと思いました。

予想外の効果

今まで、TDSは毎日測ってはいなかったのですが、値が見えてしまうので、
そろそろかなという感じで、事前に水替えをするようになりました。
「見える化」のメリットが意識せずに達成した感じです。

P.S.

採用情報
■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です
■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!
メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm
■Twitterもフォローしてくださいね!
ツイッターでも随時情報発信をしています
twitter.com