アクアリウムIoT スマート電源タップを自作してみた

# TL;DR

<iframe width="560" height="315" src="https://www.youtube.com/embed/tULAVWjCUPA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 目的
筆者はアクアリウムが好きです。
ライトやエアレーションといった、アクアリウムを支える機器のオンオフをスケジューリングやスマートフォンから制御したい。（本記事）

ゆくゆくはセンサーを駆使して最適な水槽環境を維持したい為、RaspberryPiを用いてソフトに制御したい。（未実証）

# 要件
* AC100V機器の電源をスケジューリングで制御できること
* AC100V機器の電源をスマートフォンから制御できること

# 方法
RaspberryPiのGPIOピンとスイッチングリレーを用いてAC100Vのスイッチングを行う。
その為に、GPIOピンから制御できる電源タップを作る(DIY要素)。
GPIOから制御できれば(=ソフトウェア的に制御できる)、あとはcrontabによるスケジュール実行とスマートフォンから制御できるようにwebサーバーをホスティングすればおk

# アーキテクチャ

![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1741324/835b2974-c243-e9ad-7199-c96b9054c883.png)


ラズパイがハードとソフトの仲介役になる形です（IoTらしさ）
リレーというのは簡単に言えば、普段人間がぽちぽち電気のON/OFFを切り替えるところを電気信号を用いて切り替える装置です。今回はその信号がラズパイのGPIOピンの5V信号となります。

あと今回制御したい機器を紹介します。（青枠内）
LEDランプ：　水草の光合成に必須。日中のみ点灯させます。
CO2添加装置： 水草の光合成に必須。LEDランプが点灯している間は共に作動させます。CO2はボンベで供給する為、有限資源（？）です。
エアレーション： 水草やお魚の呼吸に必要です。ぶくぶく。CO2添加装置が作動していない時に作動させます。

# 実装

ここから具体的な実装や作業を記していきます。
## GPIO電源タップの作成(DIY)

:::note alert
AC100V機器の改造やDIYには危険が伴い、施工に資格が必要な場合があります。
参考にする場合は自己責任でお願いします。
:::

※ 筆者はなぜか第二種電気工事士を持っています。

### 回路図（手書き）

![iOS の画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1741324/a1fd6ff1-975b-d7a0-e3d0-b964b9dcbdfa.jpeg)

怖いんでAC100V側にはヒューズを付けました。

### 調達〜作成

主に秋葉原駅周辺にお世話になりました。はんだごても持っていなかった為、購入。
リレーについてはネット通販で購入。

### 成果物

![S__4014093.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1741324/a05749bd-ca3e-90a8-2d58-a7a31c8b8eb9.jpeg)


ちょっとごちゃついてますが、収納ボックスに入れて短絡が起きないよう固定して使っています。
※ **最低限の絶縁しか施していないので、お子さんやペットがいる家庭は要注意**

## スケジュール実行（crontab）

OSの機能であるcrontabでGPIOを制御します。単純。
こんなかんじ

```
# 2:LED
00 18 * * * gpio -g write 2 1
0 23 * * * gpio -g write 2 0
# 3:CO2
00 18 * * * gpio -g write 3 1
50 22 * * * gpio -g write 3 0

# 4:AIR
0 23 * * * gpio -g write 4 1
00 18 * * * gpio -g write 4 0
```

## スマホアプリ

スマホからぽちぽちON/OFFできるようにします。
iOS,Android双方で使いたかった為、webアプリ（PWA）にしました。

フロントエンドはVue3を試用

バックエンドはNode
サーバー機能はKoa.js
GraphQL周りはApolloを使用
DB（SQLite）も色々あって使ってますが説明等は割愛

GraphQLのSubscription（≒プッシュ型API）を用いて他の端末からスイッチをいれた時でも画面に反映されるようにしています。


### バックエンド

スキーマ定義はこんな感じです。シンプル。

```graphql
type Gpio {
  id: Int!
  name: String
  enabled: Boolean!
}

type Query {
  version: String
  gpios(id: Int): [Gpio]
}

type Mutation {
  switchGpio(id: Int!, enable: Boolean!): Gpio
}

type Subscription {
  switchedGpio: Gpio
}
```

GPIOの操作は以下のように`exec`を使います。使いやすいようにPromise化してます。

```typescript
import { exec } from 'child_process';

export const runCommandAsync = async (command: string): Promise<{ stdout: string; stderr: string }> => {
  return new Promise((resolve, reject) => {
    exec(command, (err, stdout, stderr) => {
      if (!!err) {
        reject(err);
      }
      resolve({
        stdout: stdout,
        stderr: stderr,
      });
    });
  });
};

runCommandAsync("gpio -g write 2 1"); // GPIOの操作
```

あとはリゾルバ実装して終わり。Subscription周りはApolloの`installSubscriptionHandlers`を使えばおk

```typescript
apolloServer.installSubscriptionHandlers(httpServer);
```

###　フロントエンド

画面的には１つなので画面のソースはっつけます。

```vuejs
<template>
 <div class="container">
    <div class="switch-box" v-for="gpio in gpios" :key="gpio.id">
      <Button
        class="main-switch p-button-rounded"
        :class="{ 'button-enabled':!gpio.enabled, 'p-button-outlined':!gpio.enabled, 'p-button-secondary':!gpio.enabled }"
        style="border-radius:10rem"
        v-model="gpio.enabled"
        :label="gpio.name"
        @click="switchGpio(gpio)"
        :disabled="gpio.progress"
      />
    </div>
 </div>
</template>

<script lang="ts">
import { ref, defineComponent, Ref } from 'vue'
import Button from 'primevue/button'
import { GraphqlServiceSingleton } from '../services/grapqhqlService'
import gql from 'graphql-tag'
import { Gpio } from '../types/graphql'

type AppGpio = {
  id: number,
  name?: string | null,
  enabled: boolean;
  progress: boolean
}
export default defineComponent({
  name: 'Main',
  components: {
    Button
  },
  setup: () => {
    const gpios:Ref<AppGpio[]> = ref([])
    const toggle = ref(false)
    const graphql = GraphqlServiceSingleton.getInstance()
    graphql.query({
      query: gql`
        query{
          gpios{
            id
            name
            enabled
          }
        }
      `
    }).then((result)=>{
      gpios.value = (result.data.gpios as Gpio[]).map(gpio=>({
        id: gpio.id,
        name: gpio.name,
        enabled: gpio.enabled,
        progress: false
      }))
    })
    const switchGpio = (gpio:AppGpio) => {
      gpio.progress = true
      graphql.mutation({
        mutation: gql`
          mutation switch($id:Int!,$enable:Boolean!){
            switchGpio(id: $id enable: $enable){
              id
              name
              enabled
            }
          }
        `,
        variables:{
          id: gpio.id,
          enable: !gpio.enabled
        }
      }).finally(()=>{
        gpio.progress = false
      })
    }
    let disposable = graphql.subscribe({
      query:gql`
        subscription{
          switchedGpio{
            id
            name
            enabled
          }
        }
      `
    }).subscribe({
      next:(data:any)=>{
        const updatedGpio:Gpio = data.data.switchedGpio
        const index = gpios.value.findIndex(gpio=>gpio.id == updatedGpio.id)
        if(index >= 0){
          gpios.value[index] = {
            id: updatedGpio.id,
            name: updatedGpio.name,
            enabled: updatedGpio.enabled,
            progress: false
          }
        }
      },
      error:(error)=>{
        console.error(error)
      }
    })

    const onBeforeUnmount = () => {
      console.log("onBeforeUnmount")
      disposable?.unsubscribe()
    }
    return { toggle, gpios, switchGpio, onBeforeUnmount }
  },
})
</script>

<style>

.container {
  display: flex;
  flex-direction: column;
  justify-content:center;
  align-items: center;
}

.switch-box {
  margin-bottom: 5vh;
}

.main-switch {
  height:10vh;
  width:70vw;
  font-size: 7vw;
}

.main-switch * {
  display:inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>
```


# 展望など

* センサーと組み合わせて水槽環境を良くしてみたい。CO2センサーでCO2添加量を最適化とか。
* 自作した電源タップをメーカーに発注（きっとお高い...）するかGPIOピン専用のリレーモジュールに替えて安全性を高めたい。