一人暮らしの祖母の畑(と家)をハックする

注）感染症対策で祖母の家に行けないため、今は実家で稼働させています。ワクチン打って感染状況が落ち着いたら設置しに行きます。

## あらすじ

御年85歳の祖母はど田舎で一人暮らしをしていますが、クソ暑い中調子がいいからと真昼間に畑仕事したり、気温30度超えてるのに涼しいと言ってエアコン付けなかったりといろいろ心配です。
夜とかに電話したタイミングで「暑い時に仕事しちゃだめだよ」と何度も言っているんですが、「はいはい」と流されてしまっています(最近は熱中症で入院しかけたこともありました...)。

なので、畑仕事のログを取って、暑い時に仕事したら子供や孫に通知する仕組みを構築することにしました。
あと暑い中外で仕事したら水分補給もちゃんとしてほしいので、家に帰ってきた時に「水分補給してね」って声をかけるようにもしてみました。

## 作ったもの

- 畑仕事のログ生成機

![IMG_0659.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/a3380dee-6503-538d-5589-c931d6a7a0e5.jpeg)

畑にある農具入れに設置し、扉開閉とそのタイミングでの温度・湿度を記録します
[Wio LTE JP Version](https://soracom.jp/store/5301/)、[Glove磁気スイッチ](https://www.seeedstudio.com/Grove-Magnetic-Switch.html)、[Glove温湿度センサー](https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-DHT11.html)を使いました。

- ログに応じて挨拶するボット

![IMG_0660.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/230f561c-ca45-84f6-c8ce-eb4edf5e51a4.jpeg)

家の玄関扉の**内側**に設置します。
いってらしゃい、おかえりなどの基本的な挨拶に加え、畑仕事ログを参照して仕事帰りだったら「お疲れ様。ちゃんと水分補給してね。」など声をかけます
ラズパイ、超音波センサー([HC-SR04](https://akizukidenshi.com/catalog/g/gM-11009/))、[ドアセンサースイッチ](https://akizukidenshi.com/catalog/g/gP-13371/)、家にあったスピーカー(写真には載せてないです)を使いました

## システム構成

![StructureImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/b938244b-b88c-b553-3e0c-83c1795dd7f1.png)

- Wio LTE JP Versionは農具入れの扉の開閉を検知すると、GAS(Google App Script)経由でその時の温度と湿度、扉が開いたのか閉じたのかをスプレッドシートに記録します
- GASで暑い時に畑仕事をしたかどうか判定し、行っていたら息子に通知(LINE Notify)します
- ラズパイは家の玄関の扉開閉を検知し、状況に応じてあいさつをします

## 作り方

### 畑仕事ログ生成機

#### 開発環境整備

- [Wio LTE の開発環境を準備する](https://users.soracom.io/ja-jp/guides/dev-boards/wio-lte/development-environment/)に従って準備しました。なお最初はMacbook Pro(M1)でやっていましたが、書き込み時に`$ ... mach-o, but wrong architecture `のエラーが発生して解消できなかったためWindows PCに切り替えました。

#### ハード設定

![IMG_0659.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/d18f2c38-05ad-8709-87c1-1b358c4b23db.jpeg)


- D38ポートに温湿度センサー、D28ポートに磁力センサーを接続しています
- 写真右下の黒いパネルはLTE通信アンテナです

#### 畑仕事ログ生成機のコード

参考：[ESP32で百葉箱IoT](https://qiita.com/marlex/items/3e24a2c56a00421a317a)

```main.cc
// 拡張子はccでなくino
#include <WioLTEforArduino.h>
#include <GroveDriverPack.h>
#include <TimeLib.h> //for getWioTime()

#define MAGNETIC_SWITCH_PIN (WIOLTE_D20)
#define INTERVAL            (500)

#define COLOR_SETUP 0, 10, 0
#define COLOR_NONE 0, 0, 0
#define COLOR_YELLOW2 238, 238, 0

GroveBoard Board;
GroveTempHumiDHT11 TempHumi(&Board.D38);

WioLTE Wio;

void setup()
{
  SerialUSB.println("____START____________");
  SerialUSB.println("### I/O Initialize.");
  Wio.Init();
  Wio.LedSetRGB(COLOR_SETUP);
  Wio.SyncTime("ntp.nict.jp");
  SerialUSB.println("### Power supply ON.");

  // D38ポート以外への電源供給を開始
  Wio.PowerSupplyGrove(true);
  delay(5000);
  Wio.PowerSupplyLTE(true);
  delay(5000);

  //LTEモジュールの起動
  SerialUSB.print("### Turn on or reset.");
  if (!Wio.TurnOnOrReset()) {
    SerialUSB.println(" -> NG");
    return;
  }else{
    SerialUSB.println(" -> OK");
  }

  //SORACOMへの接続
  SerialUSB.print("### Connecting to \"soracom.io\".");
  if (!Wio.Activate("soracom.io","sora","sora")) {
    SerialUSB.println(" -> NG");
    return;
  }else{
    SerialUSB.println(" -> OK");
  }

  // 温湿センサー
  SerialUSB.begin(115200);
  Board.D38.Enable();
  TempHumi.Init();

  pinMode(MAGNETIC_SWITCH_PIN, INPUT);

  Wio.LedSetRGB(COLOR_NONE);
}

boolean door_closed = false;
boolean prev_door_closed;

void loop()
{
  prev_door_closed = door_closed;
  door_closed = (digitalRead(MAGNETIC_SWITCH_PIN) == HIGH);
  /*
  * 扉が開いた
  */
  if (prev_door_closed  && !door_closed){
    SerialUSB.print("Opened : ");
    reporter(1);
  }

  /*
  * 扉が閉まった
  */
  else if (!prev_door_closed  && door_closed){
    SerialUSB.print("Closed : ");
    reporter(0);
  }
  else {
    if (door_closed){
      blinkBlue(20);
    } else {
      Wio.LedSetRGB(COLOR_YELLOW2);
    }
  }

  delay(INTERVAL);
}

// 温湿度をコンソールに出力
void showTempHumi(float temp, float humi) {
    SerialUSB.print("Current humidity = ");
    SerialUSB.print(TempHumi.Humidity);
    SerialUSB.print("%  ");
    SerialUSB.print("temperature = ");
    SerialUSB.print(TempHumi.Temperature);
    SerialUSB.println("C");
}

// 温湿度を取得し、ログ作成関数に引き渡し
void reporter(int isOpened){
    TempHumi.Read();
    float temp = TempHumi.Temperature;
    float humi = TempHumi.Humidity;
    showTempHumi(temp, humi);
    if (isOpened){
        makeLog(1, 0, temp, humi);
    }
    else {
        makeLog(0, 1, temp, humi);
    }
}

// LED点滅
void blinkBlue (int Interval){
  Wio.LedSetRGB( 1 , 8 , 8 );
  delay(Interval);
  Wio.LedSetRGB( 22 , 110 , 114 );
  delay(Interval);
  Wio.LedSetRGB( 52 , 205 , 215 );
  delay(250);
  Wio.LedSetRGB( 39 , 192 , 200 );
  delay(Interval);
  Wio.LedSetRGB( 1 , 8 , 8 );
  delay(Interval);
}

// スプレッドシートに書き込み
void makeLog(int isOpened, int isClosed, float temperature, float humidity){
    char buf[256];
    char url[512];
    sprintf(url,
        "https://script.google.com/macros/s/SHEET_ID/exec?p1=%d&p2=%d&p3=%f&p4=%f", // SHEET_IDはスプレッドシートのID
        isOpened,
        isClosed,
        temperature,
        humidity
    );

    // URLにパラメータを付けて送信
    int httpResult = Wio.HttpGet(
        url,
        buf,
        sizeof(buf),
        60000
    );

    SerialUSB.println("Status : " + httpResult); // 多分表示されない(下のトラブル参照)
    SerialUSB.println(url);
    SerialUSB.println(buf);

}
```

##### トラブル

```makeLog.cc

void makeLog(int isOpened, int isClosed, float temperature, float humidity){
    char buf[256];
    char url[512];
    sprintf(url,
        "https://script.google.com/macros/s/SHEET_ID/exec?p1=%d&p2=%d&p3=%f&p4=%f", // SHEET_IDはスプレッドシートのID
        isOpened,
        isClosed,
        temperature,
        humidity
    );

    int httpResult = Wio.HttpGet(
        url,
        buf,
        sizeof(buf),
        60000
    );

    SerialUSB.println("Status : " + httpResult);
    SerialUSB.println(url);
    SerialUSB.println(buf);
}
```

こちらで`Wio.HttpGet()`([リファレンス](https://seeedjp.github.io/Wiki/Wio_LTE_for_Arduino/Reference-ja.html))の戻り値をhttpResultで受けていますが、なぜか自分の環境では戻り値が空になっていました。
書き込み自体は成功しているので一旦放置していますが、時間があれば調べてみたいと思います。

### GAS

参考：[GASでGetパラメータを受け取ってスプレッドシートに書き込む方法](https://qiita.com/hirohiro77/items/a947416f803f45777338)
参考：[GASでLINE Notifyする関数メモ](https://qiita.com/n0bisuke/items/6185855a827786934577)

```main.js

const ID = PropertiesService.getScriptProperties().getProperty('SHEET_ID') // プロパティに設定したシートIDを取得
const sheet = SpreadsheetApp.openById(ID).getSheetByName("Records");

const LINE_NOTIFY_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_NOTIFY_TOKEN') // プロパティに設定したLINE Notify Tokenを取得

function doGet(e) {
  //JSONオブジェクト格納用の入れ物
  let rowData = {};

  if (e.parameter == undefined) {
      //エラーはJSONで返すつもりなので
      rowData.value = '0';
      const result = JSON.stringify(rowData);
      return ContentService.createTextOutput(result);

  }else{
      const date = new Date();
      const array = [Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'), e.parameter.p1 , e.parameter.p2 , e.parameter.p3 , e.parameter.p4 ];

      //シートに配列を書き込み
      sheet.appendRow(array);

      if (e.parameter.p1 == '1' && e.parameter.p2 == '0'){
          // 農具入れが開いた
          lineNotify("畑仕事が始まりました！")
      } else if (e.parameter.p1 == '0' && e.parameter.p2 == '1'){
          // 農具入れが閉じられた
          lineNotify("畑仕事が完了しました！")
      } else {
          // 何もしない
      }

      rowData.value = '1';
      const result = JSON.stringify(rowData);
      return ContentService.createTextOutput(result);
  }
}

function lineNotify(postText){
  try {
    const url  = 'https://notify-api.line.me/api/notify'
    const token = LINE_NOTIFY_TOKEN;
    const params = {
      method: 'post',
      headers: {
        'Authorization': 'Bearer '+ token
      },
      payload: {
        message : postText
      }
    }

   const res = UrlFetchApp.fetch(url, params);
   console.log(res);

  } catch (error) {

    console.log(error);

  }
}

```

書き込み例：

![スクリーンショット 2021-08-22 16.12.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/c674a410-8b84-fc39-d608-879fc53393b4.png)

### ログに応じて挨拶するボット

#### ラズパイ用開発環境整備

ラズパイの開発環境は
- [Raspberry Pi 初期設定](https://qiita.com/HeRo/items/c1c30d7267faeb304538)
- [macだけでRaspberry Piに接続してみた](https://qiita.com/murs313/items/81757bf8bc74b6b76cdf)
を参考にしました。

#### ハード設定

![IMG_0660.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304608/9e75e354-0795-377c-2bfd-87bc0a5e556a.jpeg)

超音波センサー(HC-SR04)は[超音波センサーで、近くに来た物体だけを撮影してみました](https://dev.classmethod.jp/articles/distance-detection-camera/)を参考に、
VCC -> 5V,
Trig -> GPIO 17,
Echo -> GPIO 27,
GND -> GND(物理番号6)
のように接続しています。

磁気センサーは3.3VとGPIO 23で接続しています。

なお超音波センサーを人感センサーとして使うことで、
扉が開いた際に人がいる = 家から出る
同 いない = 外から帰ってきた
という判定を行うようにしています。

#### ログに応じて挨拶するボットのコード

```main.py
# -*- coding: utf-8 -*-
import time
import RPi.GPIO as GPIO
import urllib.request
import json
import datetime
import pygame.mixer


# 超音波センサー(HC-SR04)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.OUT) # Trig
GPIO.setup(27,GPIO.IN) # Echo

# 磁気センサー
# 家の内側に向けて設置
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

"""
超音波センサーでの人感知の有無

 | 開 | 閉 | 状況 |
 | --- | --- | --- |
 | 1 | 0 | 家を出る |
 | 1 | 1 | 客対応 |
 | 0 | 1 | 家に帰ってきた |
 | 0 | 0 | 忘れ物 |
"""

def getDistance():
    GPIO.output(17, GPIO.LOW)
    GPIO.output(17, True)
    time.sleep(0.00001)
    GPIO.output(17, False)
    while GPIO.input(27) == 0:
        signaloff = time.time()
    while GPIO.input(27) == 1:
        signalon = time.time()
    timepassed = signalon - signaloff
    return timepassed * (331.50 + 0.606681)* 100/2

def isInFrontOfDoor():
    distance = getDistance()
    print(f'Distance : {distance}cm.')
    if distance < 50:
        print(f'isInFrontOfDoor -> True ({distance}cm).') # 玄関の内側にいる
        return True
    else :
        print(f'isInFrontOfDoor -> False ({distance}cm).') # 玄関の外側にいる
        return False

def doorIsClosed():
    # 参考：https://arkouji.cocolog-nifty.com/blog/2020/10/post-4653a9.html
    return GPIO.input(23)

def doorClosed():
    print("Closed!")
    if isInFrontOfDoor():
        whenBackHome()
    else :
        pass
    return

def doorOpened():
    print("Opened!")
    if isInFrontOfDoor():
        playAudio("goWork") # いってらっしゃーい
    else :
        pass
    return

def cleanup():
    GPIO.cleanup()
    pygame.mixer.music.stop()
    print('Cleanup Fin.')
    return

def playAudio(fileName):
    pygame.mixer.music.load(f'{fileName}.mp3')
    pygame.mixer.music.play(1)
    return

def whenBackHome():
    def getOutsideData():
        req = urllib.request.Request('https://script.google.com/macros/s/SHEET_ID/exec')
        with urllib.request.urlopen(req) as res:
            body = res.read()
        latestInfo = json.loads(body.decode())
        latestInfo['time'] = datetime.datetime.strptime(latestInfo["time"].replace('T', ' ')[:-5], '%Y-%m-%d %H:%M:%S')
        return latestInfo

    outsideInfo = getOutsideData()

    minDiff = round((datetime.datetime.now() - outsideInfo["time"]).seconds/60)
    if (minDiff <= 30):
        if (outsideInfo["temp"] >= 28):
            playAudio("afterWorkInSummer") # 暑い中お疲れ様。水分補給してねー
        elif (outsideInfo["temp"] >= 10):
            playAudio("afterWorkNormal") # 作業お疲れ様。ちゃんと休憩取ってね。
        else :
            playAudio("afterWorkInWinter") # お疲れ様。暖房つけた部屋で暖まってね
    else :
        playAudio("wellComeBack") # おかえりなさーい
    return

def main():
    try :
        # mixerモジュールの初期化
        pygame.mixer.init()

        doorIsClosedPrev = True
        isAwake = False
        print("---- Loop Starts ----")
        while True:
            doorIsClosedNow = doorIsClosed()
            if doorIsClosedNow ==  doorIsClosedPrev:
                # 変化なし
                time.sleep(1)
            elif not doorIsClosedNow and doorIsClosedPrev:
                # 開いた
                doorOpened()
                isAwake = False
                time.sleep(1)
            elif doorIsClosedNow and not doorIsClosedPrev:
                # 閉じた
                doorClosed()
                time.sleep(1)

            doorIsClosedPrev = doorIsClosedNow
            time.sleep(0.1)
    except KeyboardInterrupt:
        print('KeyboardInterrupt')
    except Exception as e:
        print(f'other : {e}')
    finally:
        # 終了処理
        cleanup()

if __name__ == '__main__':
    main()

```

## 作ってみて

Wioを始めて使ったので環境設定に手間取ったのと、D38ポート以外を使う場合は`PowerSupplyGrove(true)`の設定が必要なことに気がつくまでにやや時間がかかりましたが、開発自体は各種センサーを使ったスケッチ例が豊富に用意されていたのでスムーズにできました。

(残念ながら感染症対策で祖母の家にはまだ行けないため)今は実家で試験的に動かしていますが、思った以上にちゃんと動いたので楽しかったです。
ただ配線丸出しで100均で買ったケースに詰めているので、筐体もちゃんと考えたいと思います。