RaspberryPi

ミニ温室IoT【ラズパイでビニールの開閉を自動化】

RaspberryPi

[PR]当サイトはアフィリエイト広告を利用しています。

今回は夏野菜の育苗用に小さな温室を作成して、ビニールの開閉をラズパイで自動化したという内容です。

なぜビニールの開閉を自動化するのか

そもそもなぜビニールの開閉を自動化したいかというと、本格的な農業用ビニールハウスでもよくあるのですが、ハウス内の温度が太陽光で上昇しすぎてしまうという問題があります。温度が上昇しすぎると生理障害が発生したり、最悪の場合、中の作物が枯れてしまいます。

そのため何らかの「無線でハウス内の温度を取得する方法」を用いない場合、一日中ハウスにつきっきりになって、温度をものすごく気にしながら何度もハウス内の温度計を見に行って、ビニールの開閉による換気をし続けなければいけません。

開けたらそのままというわけにはいかず、曇ってハウス内が冷えてきたら熱が逃げないようにビニールを閉じる必要もあります。

これでは単なる開閉という単純動作のために時間がどんどん溶けていきます。これはよろしくないです。

そこでこの開閉作業を何らかの手法で自動化できれば、単純作業に割く人間のリソースが減少して、時間ができます。

とはいえ私は大規模な農業用ビニールハウスを所持しているわけではないので、小さな10個程度の苗のポットが入るくらいのミニ温室を作成して、それの換気用の開閉を自動化します。開閉はサーボモーターで行います。

サーボモーターの制御も、温度の取得もラズパイでできるので、ラズパイ用の制御用Pythonプログラムを用いて作成していきます。

最終的にはクラウドストレージのDropboxと連携させて、現在のハウス内の温度データ、ビニールが開いているのか閉じているのか、といったデータを自動アップロードして、スマホからいつでも確認できたり、Dropbox内の指示用のファイルの値をもとに強制的に開閉ができたりするという機能もつけます。

なおクラウドストレージを使うためにはインターネット接続が必要で、ラズパイのWiFiを利用することになりますが、格安SIMとモバイルSIMフリールーターの組み合わせを利用すれば、ハウスが家のWiFiの範囲外にあってもハウス内の情報の取得と開閉指示ができます。おすすめの格安SIM(ロケットモバイル)も今回の記事内でご紹介するのでよろしければそちらもご覧ください。

実行環境はRaspberry Pi 3 Model Bで、OSはRaspberry Pi OSです。Raspberry Pi 4でも動作するでしょう。Pi 3よりスペックが高い分快適です。

ミニ温室外観

今回作成したミニ温室の外観は以下のようになります。

ビニールは透明ゴミ袋を切って100均のクリップで留めて使用します。温室の木材にがっちり固定すると細かい開閉の微調整がしにくくなるのでビニールをクリップで繋ぎ合わせる構造としました。

サーボモーターは100均の細めの木材を大きめのクリップでハウスに固定して、細い木材にサーボモーター付属のネジで留めます。

温室正面は以下のような感じです。輪ゴムで繋いだ割りばしの格子をサーボモーターで開け閉めします。

最初の起動はUSB起動できるモニターとラズパイをポータブル電源のUSB出力につないで実行します。

この状態のポータブル電源の使用電力は約5Wです。

開閉部分のサーボモーターの対になる軸は針金を曲げて輪っかを作ってそこに割りばしを差し込みました。

温度センサーのDHT22は温室内に入れておきます。

開閉用格子はこんな感じです。ビニールはすべてクリップで適当に留めました。

サーボモーターは以下のように取り付けました。

開閉確認用リードスイッチは以下のように取り付けました。接続線が出ているスイッチを二つ温室の木材に固定、対になる接続線がないスイッチを割りばしにビニールテープで貼り付けました。

強制的に「開」状態にした場合は以下のようになります。格子の右側のビニールには穴が開いています。

モニター接続用のHDMI端子をラズパイから取り外してモニターの電源をオフにしてラズパイだけの電気消費にした場合、ポータブル電源の出力電力は0Wになりほとんど減少しないことがわかります。

このことから200Wh程度の容量のポータブル電源でも十分一日もちます。Jackeryは大手なのでどのバッテリーにしようか悩んだらこのメーカーにするといいかもしれません。低容量モデルとして300 Plusと240がありますが、300 Plusはリン酸鉄リチウムイオン電池となっており約3,000回の充放電サイクルをもっているので、240より長期間使用することができます。

ソーラーパネルで充電もできるのでよりサステナブルにしたいならソーラーパネルとのセットもおすすめです。

とにかく安くポータブル電源がほしいなら在庫があるうちは240モデルでも十分です。300 Plusの前のモデルなので在庫がなくなると入手は難しくなると思います。

Jackery ポータブル電源 240

29,800円(税込)

Amazonで見る

ちなみに今回作成した自動開閉システムでどのくらいの温室の温度が維持できるのか、という話ですが、後述するSwitchBot防水温湿度計を温室に入れて開閉システムを午前10時ころから稼働させてみました。場所は縁側です。窓ごしの太陽光を当てました。

結果は太陽光が当たるとぐんぐん室温が上がりました。しかし上がり続けるということはなく、午前10時から午後2時くらいまで日差しが出続けている快晴でしたが、最高室温は午後1時30分ごろの約28度程度でした。ビニールをクリップで装着する関係で少し密閉しないくらいダブつかせてビニールをかけましたが、このくらいなら今回の開閉システムでもなんとかなるようです。

昔は育苗用の小さいビニール温室は、日差しが出ているときに開けっ放しにして一日放置みたいなこともやられていたようですし、そのくらいでうまくいくなら、とくにファンで強制的に温室内の空気を循環させなくても意外となんとかなるようです。

Python仮想環境の作成

今どきのPython実行環境は仮想環境を利用するらしいです。仮想環境に必要なモジュールをインストールしておけば、たくさん色々モジュールをインストールして、何をインストールしているのかわからなくなったり、環境が不安定になっても不安定な仮想環境を削除して別の仮想環境を作ればモジュールをリセットできて使い勝手がよいそうです。

今回のPythonファイルも仮想環境で実行していきます。

ラズパイのホームディレクトリに移動します。ターミナルを立ち上げて「cd」コマンドで移動していなければそこがホームディレクトリです。そこでターミナルに以下のコマンドを叩きます。ホームディレクトリに仮想環境を構築するコマンドです。

python -m venv .venv

もしエラーが出るなら仮想環境を作れるモジュールがない可能性があるので、以下を試してみてください。

sudo apt install python3.10-venv

特にエラーがなければ以下のコマンドをターミナルで実行します。

. .venv/bin/activate

ターミナルの現在の入力行の左端に(.venv)と表示されていれば仮想環境を実行できています。

ここからは仮想環境を実行する中で作業していきます。

pigpioのインストール

ラズパイのGPIOピンを使うので以下のpigpioモジュールをインストールします。インストールされているか確認するために以下のコマンドを実行してみてください。pigpioのデーモンを立ち上げるコマンドです。

sudo pigpiod

エラーが出なければインストールされています。もしエラーが出る場合は以下でインストールしてください。

sudo apt install pigpio

今回のプログラムを利用するにはpigpioのデーモンを立ち上げる必要があるので、プログラムが完成してCronの設定が完了したら、ログインするたびに以下のコマンドでデーモンを立ち上げましょう。

sudo pigpiod

ラズパイのGPIOピンについて

今回は電子工作なのでGPIOピンに様々な部品をつないで使います。各ピンの名称と機能については以下の記事が参考になります。

Raspberry Pi OS

今回使うのはGPIOの21番、18番、16番、26番です。また適宜Ground(GND:要するに負極)と5V powerを使います。

完成プログラム

まず完成したプログラムを載せて、その内容を一つずつ解説するという内容とします。

全体プログラムは以下となります。ファイル名は「solar-house.py」としました。これをホームディレクトリに配置します。

ホームディレクトリというのは、ラズパイのユーザーとして「tanaka」というユーザーを作った場合、「/home/tanaka/」というディレクトリとなります。

import dropbox
import csv
rdbx = dropbox.Dropbox(oauth2_refresh_token='更新トークン', app_key='アプキー', app_secret='シークレットキー')
rdbx.users_get_current_account()
dbx = dropbox.Dropbox(rdbx._oauth2_access_token)

import pigpio
import time
SERVO_PIN = 21
pi = pigpio.pi()
def set_angle(angle):
    assert 0 <= angle <= 180, '角度は0から180の間でなければなりません'
    
    # 角度を500から2500のパルス幅にマッピングする
    pulse_width = (angle / 180) * (2500 - 500) + 500
    
    # パルス幅を設定してサーボを回転させる
    pi.set_servo_pulsewidth(SERVO_PIN, pulse_width)

#温度で制御
import board
import adafruit_dht
import datetime
dhtDevice = adafruit_dht.DHT22(board.D18)
count = 0
failcount = 0
temperature_c = 0
while (count < 3) and (failcount < 3):
    try:
        temperature_c = dhtDevice.temperature
        temperature_f = temperature_c * (9 / 5) + 32
        humidity = dhtDevice.humidity
        print("Temp: {:.1f} F / {:.1f} C    Humidity: {}% ".format(temperature_f, temperature_c, humidity))
        count = count + 1
        time.sleep(2)
    except RuntimeError as error:
		# Errors happen fairly often, DHT's are hard to read, just keep going
        print(error.args[0])
        failcount = failcount + 1
        time.sleep(2.0)
        continue

for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "temperature.csv":
        fw = open('temperature.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        print("intemp: " + str(temperature_c))
        dataWriter.writerow([str(temperature_c) + " C", datetime.datetime.now()])
        fw.close()
        fw = open('temperature.csv', 'rb')
        dbx.files_upload(fw.read(), '/temperature.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

if temperature_c < 25.0:
    set_angle(145) 
    time.sleep(5)
else:
    set_angle(0)
    time.sleep(5)

#手動
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "stop0-open1-close2-control.csv":
        dbx.files_download_to_file('stop0-open1-close2-control.csv', '/stop0-open1-close2-control.csv')
        filename = 'stop0-open1-close2-control.csv'
        with open(filename, encoding='utf8', newline='') as f:
            csvreader = csv.reader(f)
            for row in csvreader:
                if (row[0] == "1"):
                    set_angle(0)
                    time.sleep(2)
                    print("Open!")
                elif (row[0] == "2"):
                    set_angle(145)
                    time.sleep(2)
                    print("Close!")            
        fw = open('stop0-open1-close2-control.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([0])
        fw.close()
        fw = open('stop0-open1-close2-control.csv', 'rb')
        dbx.files_upload(fw.read(), '/stop0-open1-close2-control.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

#開閉状態アップロード
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
sw_status1 = 1
sw_status2 = 1
try:
    GPIO.setup(16,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status1 = GPIO.input(16)
    if sw_status1 == 0:
        print("Open True")
    else:
        print("Open False")
    GPIO.setup(26,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status2 = GPIO.input(26)
    if sw_status2 == 0:
        print("Close True")
    else:
        print("Close False")   
except:
    print("Open Close Error")	

GPIO.cleanup()

OpenText = ""
CloseText = ""
if (sw_status1 == 0):
    OpenText = "Open True"
else:
    OpenText = "Open False"
if (sw_status2 == 0):
    CloseText = "Close True"
else:
    CloseText = "Close False"
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "StatusOpenClose.csv":
        fw = open('StatusOpenClose.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([OpenText, CloseText])
        fw.close()
        fw = open('StatusOpenClose.csv', 'rb')
        dbx.files_upload(fw.read(), '/StatusOpenClose.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

print("Open Close end")

サーボモーターの制御

サーボモーターというのは指定した角度まで回転させることができるモーターです。指定した角度まで回転するとその角度を保持します。電源がオフになっても角度の情報は保持されるので、電源オフ状態で0度の位置にあった場合、そこからプログラムで0度の角度を指定すると回転しません。その特性が今回の開閉と相性が良いので採用しました。

以下の記事を参考にさせていただきました。

【ラズベリーパイ入門】サーボモーターを制御する方法

参考サイトはsozorablogさんとなります。

今回使うのはSG90というサーボモーターです。

SG90

820円(税込)

Amazonで見る

ラズパイとサーボモーター・温度センサーとの間には結構距離があるのでジャンパーワイヤーがたくさんあると便利です。

ジャンパーワイヤ

699円(税込)

Amazonで見る

SG90の茶色い線をラズパイのGPIOのGround(GND)につないで、赤を5V powerにつないで、オレンジをGPIOの21番につなぎます。21番でうまくいかないときは12番でもよいでしょう。私は21番でうまくいきました。

サーボモーターの制御用関数は以下のコードとなります。関数set_angle()の引数に角度を入れればその角度までサーボモーターが回転します。

import pigpio
import time
SERVO_PIN = 21
pi = pigpio.pi()
def set_angle(angle):
    assert 0 <= angle <= 180, '角度は0から180の間でなければなりません'
    
    # 角度を500から2500のパルス幅にマッピングする
    pulse_width = (angle / 180) * (2500 - 500) + 500
    
    # パルス幅を設定してサーボを回転させる
    pi.set_servo_pulsewidth(SERVO_PIN, pulse_width)

気温の取得

気温を取得するためにDHT22というセンサーを利用します。湿度も測れるので湿度も管理したいときにも重宝します。

DHT22

998円(税込)

Amazonで見る

GPIOのつなぎ方ですが、VCCを5V powerに、GNDをGround(GND)に、DATを18番につなぎます。

プログラムの参考にしたのは以下の記事です。GitHubのコードです。

Adafruit_CircuitPython_DHT

adafruit_dhtモジュールを利用するために以下のコマンドをコンソールに入力します。仮想環境で作業してください。

sudo apt-get install libgpiod2
pip install adafruit-circuitpython-dht

このpipですが仮想環境にインストールされます。

また次のモジュールもインストールしておきます。

pip install board

DHT22で温度を取得するプログラムは以下となります。

import board
import adafruit_dht
import datetime
dhtDevice = adafruit_dht.DHT22(board.D18)
count = 0
failcount = 0
temperature_c = 0
while (count < 3) and (failcount < 3):
    try:
        temperature_c = dhtDevice.temperature
        temperature_f = temperature_c * (9 / 5) + 32
        humidity = dhtDevice.humidity
        print("Temp: {:.1f} F / {:.1f} C    Humidity: {}% ".format(temperature_f, temperature_c, humidity))
        count = count + 1
        time.sleep(2)
    except RuntimeError as error:
		# Errors happen fairly often, DHT's are hard to read, just keep going
        print(error.args[0])
        failcount = failcount + 1
        time.sleep(2.0)
        continue

board.D18というのはデータの入力をGPIOの18番のピンで行いますということです。

temperature_cに摂氏つまり(℃)を格納します。変数countとfailcountはそれぞれ読み込みに成功したたcountを一つ増やす、失敗したらfailcountを一つ増やすということをやる変数です。どちらかが3になったら繰り返しを抜けます。

なぜこんなことをやるかということですが、どうもDHT22でいきなり温度を測ると最初の一回目がちょっと異常値みたいな値になることがあるのです。だから3回目の値を採用したいのですが、これも確実に読み込めないことがあるので、失敗した回数も数えて、失敗回数が3となったら諦めるという処理としました。

これがうまくいくとtemperature_cにセンサーの温度が入ります。

温度に合わせてサーボモーターで開閉する

開閉のプログラムはシンプルです。夏野菜の苗は生育適温が25℃前後なので、25℃を境に開閉します。

if temperature_c < 25.0:
    set_angle(145) 
    time.sleep(5)
else:
    set_angle(0)
    time.sleep(5)

実際にサーボモーターを取り付けてみて、どの角度でどこまで回転するのか試しながら回転角度を微調整してください。

開閉状態をリードスイッチで取得

開閉状態を入力するためにリードスイッチを使います。リードスイッチというのは内部に磁石が入っていて、ペアのスイッチ内の磁石が近づくと回路がオンになって近づいたかどうかがわかるスイッチです。ドアの開閉を調べたりするのに使いますが、今回は温室の窓の開閉を調べるのに使います。

リードスイッチ

1,118円(税込)

Amazonで見る

リードスイッチの接続線はむき出しの針金なので、コネクタにはんだ付けします。ビニールテープでいけるかな、と思ったのですが、少し動くと接続部がずれて通電しなくなります。はんだ付けしましょう。

はんだこてセット

727円(税込)

Amazonで見る

白光(HAKKO) HEXSOL 鉛フリーはんだ

569円(税込)

Amazonで見る

1つ目のスイッチ(オープン側)の2本の線をGNDと16番につなぎます。2つ目のスイッチ(クローズ側)の2本の線をGNDと26番につなぎます。

RPi.GPIOモジュールはデフォルトでインストールされています。

リードスイッチのプログラムは以下となります。

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
sw_status1 = 1
sw_status2 = 1
try:
    GPIO.setup(16,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status1 = GPIO.input(16)
    if sw_status1 == 0:
        print("Open True")
    else:
        print("Open False")
    GPIO.setup(26,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status2 = GPIO.input(26)
    if sw_status2 == 0:
        print("Close True")
    else:
        print("Close False")   
except:
    print("Open Close Error")	

GPIO.cleanup()

GPIO.setmode(GPIO.BCM)の部分ですが、GPIO.BCMとすることで、GPIOの番号を利用すると宣言する処理となります。リードスイッチはスイッチがオンなら(つまり近づいたら)0が入力されます。オフなら1が入力されます。

これでどちらかのスイッチがオンになっていることがわかるので、その値を利用することになります。sw_status1とsw_status2の値を使っていきます。

Dropbox APIの利用

クラウドストレージとして色々なサービスがありますが、いくつか試してみて使いやすかったのはDropboxです。今のところ2GBまでの利用なら無料なので、今回の用途では十分です。

Pythonで利用するにはDropboxのAPIを使います。使い方の詳細は以下の記事を参考にしてください。

【Python】Dropbox AIPを使ってファイルを操作する

注意点としては、パーミッション(権限)のチェックを入れる際に以下の3つにチェックを入れないと、今回の「ファイルへのアクセス」と「上書き」操作でうまく動かないということです。

  • files.content.write
  • files.content.read
  • sharing.write

参考サイトに加えてsharing.writeにチェックを入れましょう。

またアクセストークンの有効期限は4時間程度しかないので、更新トークンを利用してプログラムが走るたびに再取得するようにします。以下を参考にしてください。

【Python】Dropbox APIでアクセストークンを自動更新する

上の参考サイト(ZerofromLight)では「.py」ファイルで実行するとうまくいかないことがあるので、コンソールでpythonと打ち込んで、一行一行実行しながら様々なトークンを出力・保存していきましょう。

またプログラムで使うのでDropbox内の「アプリ」というフォルダ内の今回使うフォルダ(名前はsolar-houseとしました)内に以下の3つのファイルを作ります。「ファイルを作成/アップロード」から「テキストファイル」、保存するときのファイル名の拡張子を「.csv」で保存します。

  • StatusOpenClose.csv
  • stop0-open1-close2-control.csv
  • temperature.csv

stop0-open1-close2-control.csvだけはファイル内に0を入力しておきます。じゃないと勝手に動きます。他のファイルは何も入力していない空ファイルでいいです。うまく動かないなら「N」とか適当な値を入れておいてください。0ではなくここに1とか2を保存しておけば1なら開く、2なら閉じるという処理が走ります。

今回の開閉プログラムの解説をしていきます。

import dropbox
import csv
rdbx = dropbox.Dropbox(oauth2_refresh_token='更新トークン', app_key='アプキー', app_secret='シークレットキー')
rdbx.users_get_current_account()
dbx = dropbox.Dropbox(rdbx._oauth2_access_token)

#温度出力
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "temperature.csv":
        fw = open('temperature.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        print("intemp: " + str(temperature_c))
        dataWriter.writerow([str(temperature_c) + " C", datetime.datetime.now()])
        fw.close()
        fw = open('temperature.csv', 'rb')
        dbx.files_upload(fw.read(), '/temperature.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

#強制開閉
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "stop0-open1-close2-control.csv":
        dbx.files_download_to_file('stop0-open1-close2-control.csv', '/stop0-open1-close2-control.csv')
        filename = 'stop0-open1-close2-control.csv'
        with open(filename, encoding='utf8', newline='') as f:
            csvreader = csv.reader(f)
            for row in csvreader:
                if (row[0] == "1"):
                    set_angle(0)
                    time.sleep(2)
                    print("Open!")
                elif (row[0] == "2"):
                    set_angle(145)
                    time.sleep(2)
                    print("Close!")            
        fw = open('stop0-open1-close2-control.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([0])
        fw.close()
        fw = open('stop0-open1-close2-control.csv', 'rb')
        dbx.files_upload(fw.read(), '/stop0-open1-close2-control.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

#開閉状況のアップロード        
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "StatusOpenClose.csv":
        fw = open('StatusOpenClose.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([OpenText, CloseText])
        fw.close()
        fw = open('StatusOpenClose.csv', 'rb')
        dbx.files_upload(fw.read(), '/StatusOpenClose.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

結局Dropboxを使う部分が一番複雑になりました。順に解説していきます。

import dropbox
import csv
rdbx = dropbox.Dropbox(oauth2_refresh_token='更新トークン', app_key='アプキー', app_secret='シークレットキー')
rdbx.users_get_current_account()
dbx = dropbox.Dropbox(rdbx._oauth2_access_token)

dropbox APIのアクセストークンを取得・設定する部分です。詳細は以下を参考にしてください。

【Python】Dropbox AIPを使ってファイルを操作する

【Python】Dropbox APIでアクセストークンを自動更新する

サイト名は「ZerofromLight」様です。

for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "temperature.csv":
        fw = open('temperature.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        print("intemp: " + str(temperature_c))
        dataWriter.writerow([str(temperature_c) + " C", datetime.datetime.now()])
        fw.close()
        fw = open('temperature.csv', 'rb')
        dbx.files_upload(fw.read(), '/temperature.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

entryには指定フォルダ内のファイルが一つずつ入ります。

entry.name == “temperature.csv”

この部分で温度出力用ファイルの処理に入ります。

writerowでファイルに書き込みます。カンマ「,」で区切れば横に複数セル入力できます。

fw = open(‘temperature.csv’, ‘rb’)

これはファイルをバイナリ形式で開く処理です。dbx.files_uploadで第一引数に設定するのはアップロードするファイルの中身となりますが、バイナリ形式でしか受け付けません。バイナリ形式にしたファイル内容をfw.read()で引数に設定します。

‘/temperature.csv’

この部分でDropbox内の「solar-house」内にtemperature.csvというファイルをアップロードできます。しかしながらtemperature.csvファイルは上書きしたいので

mode=dropbox.files.WriteMode.overwrite

を指定します。このoverwiteの指定を動かすために、この章で最初に示した「sharing.write」のチェックが必要なのです。

この部分の処理では、Dropbox上のファイルを直接書き換えることはできないので、いったんダウンロードしたファイルをローカルで編集して、既存のファイルに上書きするという手順で処理しています。

続いて強制開閉の部分です。

for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "stop0-open1-close2-control.csv":
        dbx.files_download_to_file('stop0-open1-close2-control.csv', '/stop0-open1-close2-control.csv')
        filename = 'stop0-open1-close2-control.csv'
        with open(filename, encoding='utf8', newline='') as f:
            csvreader = csv.reader(f)
            for row in csvreader:
                if (row[0] == "1"):
                    set_angle(0)
                    time.sleep(2)
                    print("Open!")
                elif (row[0] == "2"):
                    set_angle(145)
                    time.sleep(2)
                    print("Close!")            
        fw = open('stop0-open1-close2-control.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([0])
        fw.close()
        fw = open('stop0-open1-close2-control.csv', 'rb')
        dbx.files_upload(fw.read(), '/stop0-open1-close2-control.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

dbx.files_download_to_file(‘stop0-open1-close2-control.csv’, ‘/stop0-open1-close2-control.csv’)

この部分の第一引数が保存先のファイル名(このファイル名でホームディレクトリに保存されます)、第二引数が「solar-house」フォルダ内のダウンロードするファイルまでのパスです。

with open(filename, encoding=’utf8′, newline=”) as f:

この部分でローカルにダウンロードしたファイルを開きます。

                if (row[0] == "1"):
                    set_angle(0)
                    time.sleep(2)
                    print("Open!")
                elif (row[0] == "2"):
                    set_angle(145)
                    time.sleep(2)
                    print("Close!")          

CSVファイルから内容を読み込んでから、上の部分で1ならサーボモーターを動かして2秒停止、2でもサーボモーターを動かして2秒停止します。

dataWriter.writerow([0])

でstop0-open1-close2-control.csvファイルの内容を0に書き換えます。強制開閉ができたら0に戻しておけば、例えば5分ごとに「solar-house.py」を実行しても指示が出ていないので(つまり何もしないという指示「0」のときの処理が実行される)何もしないという処理にできます。

スマホのDropboxアプリからstop0-open1-close2-control.csvファイルを直接編集することはできないので、強制開閉したいならテキストファイルのアップロードで新規に「stop0-open1-close2-control2.csv」などの微妙に変えた名前で内容が1などのファイルを作成してから「stop0-open1-close2-control.csv」を削除して、「stop0-open1-close2-control2.csv」の名前を「「stop0-open1-close2-control.csv」」に戻すという処理が必要になります。

次は開閉状況のアップロードです。

OpenText = ""
CloseText = ""
if (sw_status1 == 0):
    OpenText = "Open True"
else:
    OpenText = "Open False"
if (sw_status2 == 0):
    CloseText = "Close True"
else:
    CloseText = "Close False"
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "StatusOpenClose.csv":
        fw = open('StatusOpenClose.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([OpenText, CloseText])
        fw.close()
        fw = open('StatusOpenClose.csv', 'rb')
        dbx.files_upload(fw.read(), '/StatusOpenClose.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()

リードスイッチの状態が入っているsw_status1とsw_status2の値をもとにテキストを作成して、それを「StatusOpenClose.csv」ファイルに上書きしています。

作成したプログラムを5分ごとに実行する

指定したファイルをある間隔で実行する機能としてCronというものがあります。

ターミナルで以下のコマンドを入力します。

crontab -e

すると1から3くらいの値を入力するように言われるので、対象のエディタを選択します。nanoエディタが使いやすいです。

するとファイル編集画面に移行するので、最終行に以下のように記述します。ラズパイのユーザー名をtanakaとし、ホームディレクトリは「/home/tanaka/」の場合の書き方は以下となります。

@reboot sudo pigpiod
*/5 * * * * cd /home/tanaka/; .venv/bin/python solar-house.py

cdでホームディレクトリへ移動してから、仮想環境のpythonを実行するために「.venv/bin/python」を指定します。

上の例では5分毎に実行していますが、2時間ごとに実行したいときなどは以下のように書きます。

0 */2 * * * cd /home/tanaka/; .venv/bin/python solar-house.py

つまりCronの書き方は

「分 時 日 月 曜日 ‘実行したいコマンドまたはスクリプト’」

このような順番で記述し、指定した時間軸で1単位ごとに実行したいなら「*」を入力します。

直接数値を入れたら、指定した時間軸の時刻に実施されます。つまり「分」のところに「23」と入力すれば23分に実行されます。

「*/時間」でその時間毎に実施されます。

現在のCronの内容を確認したいときは以下のコマンドで確認します。

crontab -l

これで5分ごとに「solar-house.py」が実行されるようになりました。

追記:2024/02/19

いちいちpigpioのデーモンを立ち上げるのが面倒だったので起動時にコマンドを自動実行するようにしました。cronのところで以下のコマンドを追加します。

@reboot sudo pigpiod

@rebootの後に実行したいコマンドを書きます。これでモニター無しでも電源を入れればプログラムが動くようになります。

プログラムが完成してCronの設定が済んだらやること

プログラムが完成し、Cronの設定が済んだら、ログインするたびにプログラム中のpigpioを利用するためにデーモンを立ち上げます。以下のコマンドを実行してから温室IOTを始めましょう。Cronに登録しても良いですが、コンソールを立ち上げてどこかにメモしたこのコマンドを実行するだけなので、今回は手動で設定することとしました。

sudo pigpiod

追記:2024/02/19

この手作業でのデーモン起動は面倒だったので起動時にcronで自動実行するようにしました。

結局今回の自動開閉処理を行うために必要なのは以下の通りです。

  • ラズパイのHDMI端子をモニターに繋ぐ
  • マウス・キーボードと各種GPIOピンをラズパイに繋ぐ
  • SIMフリールーターとラズパイをモバイルバッテリーに繋ぐ
  • ラズパイの電源を入れる
  • ラズパイにログイン出来たら上のコマンドでpigpioのデーモンを立ち上げる(cronに登録すれば不要)
  • HDMI端子を外してラズパイとSIMフリールーター、モバイルバッテリーを温室と一緒にベランダや庭、農地などに出して日光に当てる

追記:マウスとキーボードを外したかったのですが、どうもUSBをオフにする処理がうまくいかない可能性があるので外さないほうが無難です。

ハウスが離れていても格安SIMとモバイルSIMフリールーターを活用

ハウスが自宅のWiFiから離れていても、格安SIMとモバイルSIMフリールーターを活用すればネットにつなぐことができます。ハウスに鍵をかけられるとか、ハウスが外から見えにくい庭にある、などの場合しかつかえませんが、ハウス内にSIMフリールーターを入れておいて、モバイルバッテリーなどを電源にすれば、物理的に離れたハウス内の情報がわかるので、かなり便利になります。今回はミニ温室の開閉をメインにプログラミングしましたが、温度センサーだけつなげて、温度センサーのファイルアップロードの部分だけ利用するだけでもかなり便利になるでしょう。

今回利用したモバイルSIMフリールーターは以下となります。安いですがちゃんと使えます。

Aterm Wi-Fi モバイルルーター tri band MP02LN

5,780円(税込)

Amazonで見る

SIMフリールーターに導入するSIMカードですが、私がおすすめするのはロケットモバイルです。神プランが月額税込328円で容量無制限に利用できるので、速度が低速の200kbpsに固定されますが、今回の処理では十分です。実際にロケットモバイルをモバイルSIMフリールーターで利用してラズパイに繋いでみましたが、十分機能しました。

月額税込328円というのは他には無い魅力的な価格です。業界最安級でしょう。

ドコモとau、ソフトバンクの回線が選べます。例えばものすごい田舎でもこれら3つの回線事業者の携帯電話のLTE通信はたいてい繋がります。つまりロケットモバイルとSIMフリールーターの組み合わせでも同じ回線を利用するので、田舎でもたいていの地域ならロケットモバイルの通信ができるということです。

離れた温室の情報を低価格で取得したいならロケットモバイルがおすすめです。

後述するSwitchBot 防水温湿度計をハブミニでWiFi接続するときもロケットモバイルとモバイルSIMフリールーターの組み合わせで離れた場所をネットに繋ぐことができます。

それほど離れていないベランダの温室の温度を測るならこんな方法もアリ

今回はラズパイにセンサーをつないで物々しくやりました。自動開閉をするためです。

しかしながら温度だけ知りたい、温度に合わせて手動でビニールを開閉したいという需要もあります。

スマホを定期的に見るということができるならば、Bluetooth通信でスマホとやり取りできる温度計が存在します。電池2つで2年駆動して防水、価格もそれほど高くないので、家に常にいるような場合はこちらでスマホをちょくちょくチェックして手動開閉するというのもアリです。

ただしWiFiを使って外出先から情報を取得するにはSwitchBotハブミニかハブ2が必要なので注意しましょう。SwitchBot 防水温湿度計単体でできるのはBluetooth通信だけです。

SwitchBot 防水温湿度計

1,782円(税込)

Amazonで見る

ハブミニがセットになったものもあります。

SwitchBot 防水温湿度計 ハブミニ

6,980円(税込)

Amazonで見る

追記【温度で自動起動するUSBファンも付けてみた】

上で温室の開閉だけでも最高温度は28度程度だったと述べましたが、植物にはそよ風が当たったほうがよいともいいますし、換気で多少なりとも温度が上がりすぎない工夫をしたいと考えました。

そこでUSBファンをラズパイのUSB端子に取り付けて、USB端子のオンオフをラズパイからすることでこれを実現することにしました。

ELUTENG 小型ファン 4cm 静音 USB扇風機

1,036円(税込)

Amazonで見る

小さいファンですが、あまり大きくしてしまうとラズパイのUSBに流せる電流を超えてラズパイが壊れてしまうので最小サイズのファンを最低出力で使います。割りばしにファンを輪ゴムで取り付けて、割りばしを温室のビニールにクリップで留めました。

プログラムは以下となります。

#手動
mode = 0
for entry in dbx.files_list_folder('').entries:
    print(entry.name)
    if entry.name == "stop0-open1-close2-control.csv":
        dbx.files_download_to_file('stop0-open1-close2-control.csv', '/stop0-open1-close2-control.csv')
        filename = 'stop0-open1-close2-control.csv'
        with open(filename, encoding='utf8', newline='') as f:
            csvreader = csv.reader(f)
            for row in csvreader:
                mode = row[0]
                if (row[0] == "1"):
                    set_angle(0)
                    time.sleep(2)
                    print("Open!")
                elif (row[0] == "2"):
                    set_angle(145)
                    time.sleep(2)
                    print("Close!")            
        fw = open('stop0-open1-close2-control.csv', 'w', encoding='utf-8', newline='')
        dataWriter = csv.writer(fw)
        dataWriter.writerow([0])
        fw.close()
        fw = open('stop0-open1-close2-control.csv', 'rb')
        dbx.files_upload(fw.read(), '/stop0-open1-close2-control.csv', mode=dropbox.files.WriteMode.overwrite)
        fw.close()
#ファンをオンオフ
import os
if (mode != "4"):
    for i in range(6):
        try:
            temperature_c = dhtDevice.temperature
            print(temperature_c)
            if temperature_c > 28.0:
                os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 1')
                print('temperature high')
            else:
                os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')
                print('temperature low')
        except RuntimeError as error:
            # Errors happen fairly often, DHT's are hard to read, just keep going
            os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')
            print('temperature not found')
        time.sleep(30)
    os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')
else:
    os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 1')

手動開閉の部分に変数modeの処理を追加して、modeが4でないなら28度より高い室温でファンをオンにします。それ以外ではオフにします。

しかしながらオフにしている間はUSB端子がすべてオフになってしまうため、キーボードもマウスも使えなくなります。

それだとよくないので、ラズパイの指示ファイルの中身に4を指定した場合に、エクストラモードとしてUSBをオンにする処理を加えました。

ラズパイのCronを設定している状態でこのプログラムを実行すると、起動5分程度でUSBがオフになってしまうので、プログラムを改修するときはファイル名を「solar-house2.py」などと変えておいて、Cron実行ファイルを停止してください。またmodeが4の時の処理がないとラズパイの操作がまったくできないので気を付けましょう。

なお

os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')

の部分などUSBのオンオフを実行する部分の詳細は以下を参考にしました。

Raspberry Pi の USB 電源を ON/OFFmofu犬blog様)

この記事にあるように、ラズパイ3にはUSB端子を個別にオンオフする機能がないです。

私の環境ではUSBハブを3番のUSBにつないでオフにしてみたところ、マウスはオフになりましたが、USBファンは止まりませんでした。

結局すべてのUSB端子をオフにして、mode=4のエクストラモードで外部からオンにするという処理としました。

追記2【Ver.1.1】プログラムの更新(2024/02/17)

しばらく作成した温室を稼働させてみました。するとWi-Fiが不安定になって接続が切れると、上のプログラムはエラーとなり操作不能となり、その状態で温室が閉まったままになり温度が上がり続けるという現象が起きてしまいました。

そこでWi-Fiが切れた時でも動作するように以下のように全体を修正しました。exmodeという変数を利用し、ネット関係のプログラムの部分でエラーになったらネット関係の処理をスキップするようにしました。今後も少しずつプログラムを改善しよりよいものにしていく予定です。

import dropbox
import csv
exmode = 0
try:
   rdbx = dropbox.Dropbox(oauth2_refresh_token='更新トークン', app_key='アプキー', app_secret='シークレットキー')
   rdbx.users_get_current_account()
   dbx = dropbox.Dropbox(rdbx._oauth2_access_token)
except:
    exmode = 1
import pigpio
import time
SERVO_PIN = 21
pi = pigpio.pi()
def set_angle(angle):
    assert 0 <= angle <= 180, '角度は0から180の間でなければなりません'
    
    # 角度を500から2500のパルス幅にマッピングする
    pulse_width = (angle / 180) * (2500 - 500) + 500
    
    # パルス幅を設定してサーボを回転させる
    pi.set_servo_pulsewidth(SERVO_PIN, pulse_width)

#温度で制御
import board
import adafruit_dht
import datetime
dhtDevice = adafruit_dht.DHT22(board.D18)
count = 0
failcount = 0
temperature_c = 30
while (count < 3) and (failcount < 3):
    try:
        temperature_c = dhtDevice.temperature
        temperature_f = temperature_c * (9 / 5) + 32
        humidity = dhtDevice.humidity
        print("Temp: {:.1f} F / {:.1f} C    Humidity: {}% ".format(temperature_f, temperature_c, humidity))
        count = count + 1
        time.sleep(2)
    except RuntimeError as error:
		# Errors happen fairly often, DHT's are hard to read, just keep going
        print(error.args[0])
        failcount = failcount + 1
        time.sleep(2.0)
        continue
if exmode == 0:
    for entry in dbx.files_list_folder('').entries:
        print(entry.name)
        if entry.name == "temperature.csv":
            fw = open('temperature.csv', 'w', encoding='utf-8', newline='')
            dataWriter = csv.writer(fw)
            print("intemp: " + str(temperature_c))
            dataWriter.writerow([str(temperature_c) + " C", datetime.datetime.now()])
            fw.close()
            fw = open('temperature.csv', 'rb')
            dbx.files_upload(fw.read(), '/temperature.csv', mode=dropbox.files.WriteMode.overwrite)
            fw.close()

if temperature_c < 25.0:
    set_angle(145) 
    time.sleep(5)
    print("close manual")
else:
    set_angle(0)
    time.sleep(5)
    print("open manual")

#手動
mode = 0
if exmode == 0:
    for entry in dbx.files_list_folder('').entries:
        print(entry.name)
        if entry.name == "stop0-open1-close2-control.csv":
            dbx.files_download_to_file('stop0-open1-close2-control.csv', '/stop0-open1-close2-control.csv')
            filename = 'stop0-open1-close2-control.csv'
            with open(filename, encoding='utf8', newline='') as f:
                csvreader = csv.reader(f)
                for row in csvreader:
                    mode = row[0]
                    if (row[0] == "1"):
                        set_angle(0)
                        time.sleep(2)
                        print("Open!")
                    elif (row[0] == "2"):
                        set_angle(145)
                        time.sleep(2)
                        print("Close!")            
            fw = open('stop0-open1-close2-control.csv', 'w', encoding='utf-8', newline='')
            dataWriter = csv.writer(fw)
            dataWriter.writerow([0])
            fw.close()
            fw = open('stop0-open1-close2-control.csv', 'rb')
            dbx.files_upload(fw.read(), '/stop0-open1-close2-control.csv', mode=dropbox.files.WriteMode.overwrite)
            fw.close()

#開閉状態アップロード
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
sw_status1 = 1
sw_status2 = 1
try:
    GPIO.setup(16,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status1 = GPIO.input(16)
    if sw_status1 == 0:
        print("Open True")
    else:
        print("Open False")
    GPIO.setup(26,GPIO.IN,pull_up_down=GPIO.PUD_UP)
    sw_status2 = GPIO.input(26)
    if sw_status2 == 0:
        print("Close True")
    else:
        print("Close False")   
except:
    print("Open Close Error")	

GPIO.cleanup()

OpenText = ""
CloseText = ""
if (sw_status1 == 0):
    OpenText = "Open True"
else:
    OpenText = "Open False"
if (sw_status2 == 0):
    CloseText = "Close True"
else:
    CloseText = "Close False"
if exmode == 0:
    for entry in dbx.files_list_folder('').entries:
        print(entry.name)
        if entry.name == "StatusOpenClose.csv":
            fw = open('StatusOpenClose.csv', 'w', encoding='utf-8', newline='')
            dataWriter = csv.writer(fw)
            dataWriter.writerow([OpenText, CloseText])
            fw.close()
            fw = open('StatusOpenClose.csv', 'rb')
            dbx.files_upload(fw.read(), '/StatusOpenClose.csv', mode=dropbox.files.WriteMode.overwrite)
            fw.close()

#ファンをオンオフ
import os
if (mode != "4"):
    for i in range(6):
        try:
            temperature_c = dhtDevice.temperature
            print(temperature_c)
            if temperature_c > 28.0:
                os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 1')
                print('temperature high')
            else:
                os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')
                print('temperature low')
        except RuntimeError as error:
            # Errors happen fairly often, DHT's are hard to read, just keep going
            os.system('sudo ./hub-ctrl -b 1 -d 2 -P 3 -p 0')
            print('temperature not found')
        time.sleep(30)
    os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 0')
else:
    os.system('sudo ./hub-ctrl -b 1 -d 2 -P 2 -p 1')
print("Open Close end")

まとめ【自動開閉できればハウス内の温度管理の手間が減る】

今回はミニ温室の開閉作業を自動化して温室のビニールの開閉に費やす手間を削減するという内容で書いていきました。

そろそろ夏野菜の苗の育苗が始まります。今回の仕組みを利用して、今までより簡単に育苗してみたいですね。

温度の取得だけ取り入れても格段に便利になるはずです。少しPythonが使えればプログラムはそれほど難しくありません。当ブログでも入門用コンテンツをご用意しております。タグの「Pythonゼロから」もご覧いただけましたら幸いです。

今回の記事でピンとくることがあったらお試しください。何かのお役に立ちましたら幸いです。

手軽にできる農業DXです