エアコンをHackしてスマートリモコンをつくる

エアコンつけたいけどリモコンに手が届かない……という経験はないでしょうか。エアコンでなくとも似たような経験がある方は多いと思います。 そこで、より怠惰な生活を送るためにRaspberry Piを使ってスマートフォンからエアコンを操作できるシステムを作ることにしました。  
![システム概要図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/7fa9a2a3-4609-f64d-8a1b-c616e977a6f1.jpeg) 
これがシステムの概要図になります。拡張性のためにDiscordを介してRaspberry Piにメッセージを送信するようにしています。 
エアコンは赤外線通信によって操作します。なのでライトやテレビのリモコンのような赤外線通信を行うリモコンであれば同じ要領でスマートリモコン化することができます。
## 1. ラズパイのセットアップ
Raspberry PiをWi-Fiに接続しSSHを利用可能にします。
既にわかりやすい記事を書いてくださっている方がいらっしゃるのでここは割愛します。

- [Raspberry Pi 初期設定](https://qiita.com/HeRo/items/c1c30d7267faeb304538)
- [Raspberry PiをHeadlessでセットアップする](https://qiita.com/rhene/items/828858afae699c5ac0ab)

## 2. 赤外線送受信回路を作る
とてもわかりやすい記事を書いてくださっている方がいらっしゃるのでここも割愛します。

- [格安スマートリモコンの作り方](https://qiita.com/takjg/items/e6b8af53421be54b62c9#%E9%9B%BB%E5%AD%90%E5%B7%A5%E4%BD%9C%E3%81%AE%E5%9F%BA%E6%9C%AC)

## 3. エアコンのリモコン信号を解析する
### 3.1 信号解析の必要性
記録した信号を送信すればエアコンを単純にON,OFFすることはできます。しかし、細かな操作をすることはできません。
というのも、エアコンのリモコンは、設定温度、運転モードなどすべてのデータを毎回エアコン本体に送信しているためです。例えば、夏場に冷房で運転しているときにエアコンをONにする信号を記録した場合、冬場に同じ信号でエアコンをONにすると冷房でエアコンが起動してしまいます。
これではあまりに使い勝手が悪いため、エアコンの信号を解析することでエアコンの状態の管理・信号の生成をRaspberry Pi側で行えるようにして実用に耐える使い勝手を実現しようというわけです。

### 3.2 リモコン信号のフォーマット
まずエアコンのリモコン信号がどのようになっているか簡単に解説します。

- [赤外線リモコンの通信フォーマット](http://elm-chan.org/docs/ir_format.html)

リモコン信号のフォーマットはこのページで詳しく解説されています。
日本製のエアコンのフォーマットは

- NECフォーマット
- 家製協(AEHA)フォーマット
- SONYフォーマット

の3種類に分類されます。以下の図はそれぞれのフォーマットでの信号の先頭部分です。
![format.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/8197b539-7e2d-f49a-96f0-cb11f9c193cf.jpeg)

まずはエアコンがどのプロトコルを使用しているかを特定しましょう。
irrp.pyを使って信号を記録します。

```bash
 python3 irrp.py -r -g18 -f codes --no-confirm aircon:on
```
それぞれのオプションの意味は以下の通りです。

| オプション | 意味 |
|:-|:-|
| -r  |　受信した信号を記録するモード  |
| -g18 | GPIO18を使って信号を受信する  |
| -f codes  | 信号を記録するファイルを指定する(ここではcodesを指定) |
| --no-confirm  | 確認のために同じ信号を要求しない |
| aircon:on | 信号の識別子 |
試しに、エアコンを起動する信号を記録してみます。

```python
{"aircon:on": [3260, 1525, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 361, 1168, 437, 438, 361, 1168, 361, 1168, 437, 1168, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361]}
```
このような信号が取れればOKです。この数列は赤外線が「ONの時間、OFFの時間、ONの時間……」という意味です。
図に書いてみるとこのようになっています。
![airconon.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/63d42951-375e-9792-a461-6c88df560ce5.jpeg)
もうひとつの上の図と照らし合わせてみると $1T \approx 400$程度とすれば$8T \approx 3200$,$4T \approx 1600$となるので、このエアコンは家製協(AEHA)フォーマットを使用しているようです。

### 3.3 信号を解析する
信号のフォーマットがわかったところで、いよいよ信号のどの部分で何を制御しているか解析していきます。
まず、このままでは長すぎて見にくいので以下のコードで信号を16進数に変換してみます。

```python:decode.py
import json
import os
import argparse

# オプション引数の定義
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file", help="Filename", required=True)
parser.add_argument("-s", "--save", help="saveFilename", type=str)

# オプション引数の取り出し
args = parser.parse_args()

# 指定されたファイルをjson形式で読み込み
with open(args.file, "r") as f:
    rowRecodes = json.load(f)

# ファイルに保存されている全ての信号を16進数に変換する
for key in rowRecodes.keys():
    interval = rowRecodes[key]

    # 信号間隔を読み取って2進数に変換(AEHAフォーマット)
    t = 0.0
    binCode = []
    for i in range(0, len(interval)-1, 2):
        if t * 7.0 < interval[i]:
            t = (interval[i] + interval[i+1]) / 12.0
            binCode.append("")
        elif interval[i+1] < t * 2.0:
            binCode[-1] += "0"
        elif interval[i+1] < t * 6.0:
            binCode[-1] += "1"
    
    # 16進数に変換
    hexCode = ""
    for c in binCode:
        hexCode += format(int(c, 2), "x")
        
    print(key + " : " + hexCode)
    # ファイルがない場合 空のjsonファイルを作成
    if not os.path.isfile(args.save): 
        with open(args.save, "w") as s:
            s.write("{}")

    # ファイルが空の場合 空のjsonファイルを作成
    with open(args.save, "r") as s:
        if len(s.read()) == 0:
            s.write("{}")

    # -s で指定されたファイルに書き込み
    with open(args.save, "r") as s:
        hexRecodes = json.load(s)
    with open(args.save, "w") as s:
        hexRecodes[key] = hexCode
        s.write(json.dumps(hexRecodes))
```
```bash
python3 decode.py -f codes -s codes_hex
```
それぞれのオプションの意味は以下の通りです。

| オプション | 意味 |
|:-|:-|
| -f codes  | 信号が記録されたファイル(ここではcodesを指定) |
| -s codes_hex | 変換した信号を記録するファイル(ここではcodes_hexを指定) |

以下のように変換されていればOKです。

```python
{"aircon:on": "4a75c358a76f90af50ff00b748ef10fd02de21"}
```

しかしこれだけでは、どこで何を制御しているかわからないのでいくつか信号をとってみて差分を見ることにします。
試しに温度を変える信号をいくつかとってみましょう。
<b>信号の規則はエアコンの機種によって全く異なると思うので、ここからはあくまで一例だと思って読んでいただければ幸いです。</b>


```python
{
    "aircon:cool29": "4a75c358a76f90cf30ff00b748ef10fd02de21",
    "aircon:cool28": "4a75c358a76f902fd0ff00b748ef10fd02de21",
    "aircon:cool27": "4a75c358a76f90af50ff00b748ef10fd02de21",
    "aircon:cool26": "4a75c358a76f906f90ff00b748ef10fd02de21",
    "aircon:cool25": "4a75c358a76f90ef10ff00b748ef10fd02de21",
    "aircon:cool18": "4a75c358a76f907f80ff00b748ef10fd02de21",
}
```
冷房で29度から25度まで1度ずつ変えて信号を記録してみました。最低温度である18度も記録しています。

```python
{
    "aircon:cool29": "4a75c358a76f90 < cf30 > ff00b748ef10fd02de21",
    "aircon:cool28": "4a75c358a76f90 < 2fd0 > ff00b748ef10fd02de21",
    "aircon:cool27": "4a75c358a76f90 < af50 > ff00b748ef10fd02de21",
    "aircon:cool26": "4a75c358a76f90 < 6f90 > ff00b748ef10fd02de21",
    "aircon:cool25": "4a75c358a76f90 < ef10 > ff00b748ef10fd02de21",
    "aircon:cool18": "4a75c358a76f90 < 7f80 > ff00b748ef10fd02de21",
}
```
信号列をぐっと睨むと`<>`で囲んだ部分が変わっていることがわかります。どうやらここが温度を制御している部分のようです。
これだけでは規則性がつかめないので、変わっている部分だけ抜き出して観察してみます。

| 温度 | 16進 | 2進 | 2進(前後逆転) | 16進(前後逆転) |
|:-:|:-:|:-:|:-:|:-:|
| 29 | cf30 | 1100 1111 0011 0000 | 0000 1100 1111 0011 | 0cf3 |
| 28 | 2fd0 | 0010 1111 1101 0000 | 0000 1011 1111 0100 | 0bf4 |
| 27 | af50 | 1010 1111 0101 0000 | 0000 1010 1111 0101 | 0af5 |
| 26 | 6f90 | 0110 1111 1001 0000 | 0000 1001 1111 0110 | 09f6 |
| 25 | ef10 | 1110 1111 0001 0000 | 0000 1000 1111 0111 | 08f7 |
| 18 | 7f80 | 0111 1111 1000 0000 | 0000 0001 1111 1110 | 01fe |


ガチャガチャと操作してみると16進(前後逆転)では温度を$T$とすれば 

|0|~(32-T)|f|32-T|
|-|-|-|-|

という規則性があることがわかりました。(`~`はbit反転の意)
運転モードについても同様に解析していきます。温度は20度に固定し運転モードを変えて信号をとってみました。

```python
{
    "aircon:on20":    "4a75c358a7 < 6f90 > 3fc0ff00b748ef10fd02de21",
    "aircon:off20":   "4a75c358a7 < 7f80 > 3fc0ff00b748ef10fd02de21",
    "aircon:auto20":  "4a75c358a7 < ef10 > 3fc0ff00b748ef10fd02de21",
    "aircon:cool20":  "4a75c358a7 < 6f90 > 3fc0ff00b748ef10fd02de21",
    "aircon:dry20":   "4a75c358a7 < af50 > 3fc0ff00b748ef10fd02de21",
    "aircon:clean20": "4a75c358a7 < 2fd0 > 3fc0ff00b748ef10fd02de21",
    "aircon:warm20":  "4a75c358a7 < cf30 > 3fc0ff00b748ef10fd02de21",
}
```

よく観察すると運転モードを制御する部分の信号は以下の表の温度と同じだということがわかります。

|運転モード|　温度 |
|:-:|:-:|
|warm| 29 |
|clean| 28 | 
|dry| 27 | 
|cool| 26 | 
|auto| 25 | 
|off| 18 |

onの信号はoffにする前の状態の信号を送信しているようです。

### 3.4 信号を生成する
これらを踏まえて所望の信号を生成するプログラムを書きます。

```python3
import os 
import json

base = "4a75c358a76f907f80ff00b748ef10fd02de21"
modeNum = {"off": 18, "auto" : 25, "cool" : 26, "dry" : 27, "clean" : 28, "warm" : 29}
pulse = [[400, 400], [400, 1200]]

def conv_temp_to_hexcode(T):
    """
    設定温度を16進制御コードに変換する

    Parameters
    ----------
    T : int
        エアコンの設定温度

    Returns
    -------
    (signal1, signal2) : tuple(str,str)
        設定温度の16進制御コード
        singal2 は singal1 の補ビット
    """
    bincode = str(bin(32 - T)) # bincode = "0bxxxx"

    # 4bitに整形
    while len(bincode) < 6:
        bincode = bincode[:2] + '0' + bincode[2:]

    signal_1 = bincode[2:6] # bit部だけ取り出す
    signal_1 = (signal_1)[::-1] # reverse
    signal_2 = signal_1.translate(str.maketrans({'0': '1', '1':'0'})) # bit 反転
    signal_1 = format(int(signal_1, 2), 'x')
    signal_2 = format(int(signal_2, 2), 'x')

    return signal_1, signal_2

def conv_mode_to_hexcode(mode):
    """
    エアコンの運転モード(冷房,暖房 etc.)を16進制御コードに変換する

    Parameters
    ----------
    mode : string
        エアコンの運転モード

    Returns
    -------
    signal : str
        運転モードの16進制御コード
    """
    global modeNum
    signal = conv_temp_to_hexcode(modeNum[mode])
    return signal

def gen_code(mode, T):
    """
    エアコンの16進制御コード全体を生成する

    Parameters
    ----------
    mode : string
        エアコンの運転モード
    T : int
        エアコンの設定温度

    Returns
    -------
    hexCode : str
        エアコンの16進制御コード
    """
    global base

    # mode部を書き換え
    tmp = conv_mode_to_hexcode(mode)
    print("mode: " + tmp[0] + "f" + tmp[1] + "0")
    base = base[:10] + tmp[0] + base[11:] # 10文字目をtmp[0]でreplace
    base = base[:12] + tmp[1] + base[13:]

    # 温度部を書き換え
    tmp = conv_temp_to_hexcode(T)
    print("temp: " + tmp[0] + "f" + tmp[1] + "0")
    base = base[:14] + tmp[0] + base[15:]
    base = base[:16] + tmp[1] + base[17:]

    hexCode = base
    print("hexCode: " + hexCode)
    return hexCode

def encode(mode, T):
    """
    エアコンの16進制御コードを赤外線LEDの点灯間隔に変換する

    Parameters
    ----------
    mode : string
        エアコンの運転モード
    T : int
        エアコンの設定温度

    Returns
    -------
    None

    Note 
    -------
    赤外線LEDの点灯間隔は同ディレクトリのairconに書き込まれる
    """
    global pulse

    hexcode = gen_code(mode, T)
    signal = [3200, 1600]

    # 2進変換
    bincode = ""
    for hx in hexcode:
        code = str(bin(int(hx, 16)))
        while len(code) < 6:
            code = code[:2] + '0' + code[2:]
        code = code[2:6]
        bincode += code
    
    # 2進 -> 点灯間隔
    sig = [pulse[int(bit)][i] for bit in bincode for i in range(0,2)]
    signal.extend(sig)
    signal.append(400)
    #print(signal)

    # ファイルがない場合 空のjsonファイルを作成
    if not os.path.isfile("aircon"): 
        with open("aircon", "w") as s:
            s.write("{}")
    # ファイルが空の場合 空のjsonファイルを作成
    with open("aircon", "r") as s:
        if len(s.read()) == 0:
            s.write("{}")
    # -s で指定されたファイルに書き込み
    with open("aircon", "r") as s:
        Recode = json.load(s)
    with open("aircon", "w") as s:
        Recode["aircon:op"] = signal
        s.write(json.dumps(Recode))

if __name__ == '__main__':
    mode = input()
    T    = int(input())

    encode(mode, T)
```

書きました。

```bash
python3 encode.py
```

```bash:input
cool
27
```

```bash:output
mode: 6f90
temp: af50
hexCode: 4a75c358a76f90af50ff00b748ef10fd02de21
```
動かしてみてこのように出力されていればOKです。元の信号間隔の状態のコードが同ディレクトリの`aircon`というファイルに出力されているはずです。

```python:aircon
{"aircon:op": [3260, 1525, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 361, 1168, 437, 438, 361, 1168, 361, 1168, 437, 1168, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 437, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361, 438, 361, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 437, 438, 361, 1168, 437, 438, 361, 438, 361, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 361, 1168, 437, 1168, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 1168, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 1168, 437, 1168, 437, 438, 361, 1168, 437, 1168, 437, 1168, 437, 1168, 437, 438, 361, 438, 361, 438, 361, 1168, 437, 438, 361, 438, 361, 438, 361, 438, 361, 1168, 361]}
```

あとはお好みで風向きや風量を制御する部分を解析して同じようにエンコードしてみてください。
コードはすべてこちらのリポジトリにあげてあります。

- [ugis70194/smart-remocon](https://github.com/ugis70194/smart-remocon)

#### 参考

- [エアコンのリモコン信号を解析する ~デコード編~](https://qiita.com/gorohash/items/598d69a63bd6b4308291)
- [Slack経由で家の外からエアコンをon, offできる装置を、Raspberry Piで作ってみた。（しかも御坂美琴ちゃんが応答してくれる）](https://qiita.com/KAKY/items/55e6c54fa2073cdc0bbe)

## 4. Discord Botを作る
### 4.1 Botアカウントの作成
メッセージを監視するためのDiscord Botを作成します。 
まずはDiscordのサーバーを作成します。
Discordで `サーバーの追加 > オリジナルの作成 > 自分と友達のため > 新規作成`　で新しいサーバーを作成します。名前はなんでもいいです。
![discord_newserver.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/299a2bcb-beba-c57c-3527-32f455729a0d.png)
次に[Discord Developers Portal](https://discord.com/developers/applications)へアクセスし、BOTのアカウントを作成します。Developers PortalからログインするとBrowser版のホームに飛ばされますが、再度アクセスすればDeveloper Portalへアクセスできます。
Portalへアクセスできたら `Applications > New Application > Create`で新しいApplicationを作成します。
![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/45d8fa6d-c271-024c-e67e-bfdfadf3de5a.png)
ここではApplicationの名前をremocon_testとしました。`Bot > Add Bot > Yes, do it!`でApplicationをBotとして登録します。
(testのような多く使われている名前だとBotの名前として使用できないことがあるようです)

### 4.2 Botアカウントの設定
Bot登録が完了すると、Botの設定画面が現れるはずです。PUBLIC BOTはOFFにしておきましょう。
![public.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/1399fbb3-7930-2754-0c55-a9d485c9e844.png)
TOKENをコピーしてBotを動かしてみます。
OAuth2のOAuth2 URL GeneratorでBotの権限を設定します。
![bot_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/718cb13b-8678-a980-07b4-63087df569e8.png)
SCOPESはbot、BOT PERMISSIONSはView Channels, SendMessagesを選択します。これはメッセージの読み取りと、送信の権限です。
生成されたURLをコピーし、アドレスバーにペーストします。
![bot_in.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/2b16469d-8fe5-ec90-d745-6bdcd1568ebf.png)
するとこのようなページに飛ぶのでBotを追加したサーバーを選択してはいを押します。
![bot_check.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/9066cc92-f937-f2ee-769f-b3663ac1095f.png)
権限はこの２つが設定されていればOKです。認証を押して次に進みます。
Botの認証が完了すれば、選択したサーバーにBotが参加しているはずです。これでBotの設定は完了です。

#### 参考

- [Pythonで実用Discord Bot(discordpy解説)](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)
- [Discord Botアカウント初期設定ガイド for Developer](https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf)

### 4.3 Botのテスト
Botのプログラムを組んでテストしてみます。

```python:discord_test.py
import discord

TOKEN = "Paste Your TOKEN"
client = discord.Client()

@client.event
async def on_message(message):
    if message.content == "ON":
        await message.channel.send("message is ON")
    elif message.content == "OFF":
        await message.channel.send("message is OFF")

client.run(TOKEN)
```
TOKENは自分のBotのTOKENに置き換えてください。TOKENはBotのページからコピーできます。
`discord`はプリインストールされていないので

```bash
pip3 install discord
```

でインストールしてください。

![token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/819113ec-e326-7934-35ff-e85d24bbbeac.png)

```bash
python3 discord_test.py
```

でプログラムを実行すればBotがオンラインになるはずです。
試しに`ON`と送信してみましょう。
![dousa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/12626d48-35ff-5551-825a-f576e8b914c4.png)
Botからメッセージが返ってきました！これでBotのテストは完了です。

### 4.4 メッセージに対応した信号を送信する

`subprocess`をつかって`irrp.py`を呼び出し、信号を送信します。

```python:server.py
import discord
import subprocess
import encode

TOKEN = "Paste Your TOKEN"
client = discord.Client()

@client.event
async def on_message(message):
    if message.content == "ON":
        encode.encode('cool', 27)
        subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./aircon", "aircon:op"])
        await message.channel.send("message is ON")
    elif message.content == "OFF":
        encode.encode('off', 27)
        subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./aircon", "aircon:op"])
        await message.channel.send("message is OFF")

client.run(TOKEN)
```
これでエアコンのON,OFFができればOKです。

## 5. エアコンの状態をラズパイ側で管理する
ここまででエアコンの単純なON,OFFはできるようになりました。ここからはエアコンの状態を管理できるようにします。
SQLiteをインストールしDBを作成します。

```bash
sudo apt install sqlite
sqlite3 db.sqlite3
```

テーブルを作成し、初期データを入れておきます。

```sql
sqlite> create table user_data(aircon_mode, aircon_temp);
sqlite> insert into user_data values('cool', 27);
sqlite> select * from user_data;
off|27
sqlite> .exit
```

サーバ側からSQLiteにアクセスしてデータをやりとりできるようにします。

```python:server.py
import discord
import subprocess
import sqlite3
import encode

connect = sqlite3.connect('db.sqlite3')
cursor  = connect.cursor()

TOKEN = "Paste Your TOKEN"
client = discord.Client()

@client.event
async def on_message(message):
    if message.content == "ON":
        cursor.execute("SELECT aircon_mode, aircon_temp FROM user_data")
        state = cursor.fetchall()[0]
        mode = state[0]
        temp = state[1]
        encode.encode(mode, temp)
        cursor.execute("UPDATE user_data SET aircon_mode = ?", (mode,))
        cursor.execute("UPDATE user_data SET aircon_temp = ?", (temp,))
        subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./aircon", "aircon:op"])
        await message.channel.send("message is ON")
    elif message.content == "OFF":
        cursor.execute("SELECT aircon_temp FROM user_data")
        state = cursor.fetchall()[0]
        temp = state[0]
        encode.encode('off', temp)
        cursor.execute("UPDATE user_data SET aircon_mode = ?", ('off',))
        subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./aircon", "aircon:op"])
        await message.channel.send("message is OFF")

client.run(TOKEN)
```

これでラズパイ側でエアコンの状態を管理して利用できるようになりました。

## 6. メッセージに対応した信号を送信する

正規表現を使ってひらすらパターンを列挙していきます。正規表現についてはこちらを参照してください。

- [正規表現 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions)
- [Regular expression syntax cheatsheet - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet)

データベースの更新、エアコンの状態の取得、`irrp.py`の呼び出しは関数にしてまとめました。
Discord BotのTOKENもデータベースで管理するようにしました。
ついでにライトの制御もできるようになっています

```python
import discord
import re 
import subprocess
import sqlite3
import encode

connect = sqlite3.connect('db.sqlite3')
cursor  = connect.cursor()

cursor.execute("SELECT token from token")
TOKEN=str(cursor.fetchall()[0][0])
client = discord.Client()

def dbUpdate(column, state):
    """
    データベースの内容を更新する

    Parameters
    ----------
    column : str
        変更するカラム
    state : int or str
        セットする値
    """
    if column == 'light':
        cursor.execute("UPDATE user_data SET light = ?", (state,))
    elif column == 'aircon_temp':
        cursor.execute("UPDATE user_data SET aircon_temp = ?", (state,))
    elif column == 'aircon_mode':
        cursor.execute("UPDATE user_data SET aircon_mode = ?", (state,))

    connect.commit()

def getState():
    """
    エアコンの運転モードと設定温度を取得する

    Returns
    ----------
    state : list[str, int]
        エアコンの運転モードと設定温度
    """
    cursor.execute("SELECT aircon_mode, aircon_temp FROM user_data")
    return list(cursor.fetchall()[0])

def lightON():
    proc = subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./codes", "light:on"])

def lightOFF():
    proc = subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./codes", "light:off"])

def airconOP(state):
    encode.encode(state[0], state[1])
    proc = subprocess.run(["python3", "./irrp.py", "-p", "-g17", "-f", "./aircon", "aircon:op"])

@client.event
async def on_message(message):
    message.content = str(message.content).replace(' ', '')

    if not re.match(r'\$', message.content) is None:
        pass
    elif not re.search(r'電気\S*つけて', message.content) is None:
        lightON()
        dbUpdate('light', 1)
    elif not re.search(r'電気\S*消して', message.content) is None:
        lightOFF()
        dbUpdate('light', 0)
    elif not re.search(r'エアコン\S*つけて', message.content) is None:
        airconOP(['off', 27])
        state = getState()
        airconOP(state)
        dbUpdate('aircon_mode', state[0])
    elif not re.search(r'エアコン\S*消して', message.content) is None:
        temp = getState()[1]
        airconOP(['off', temp])
        dbUpdate('aircon_mode', 'off')
    elif not re.search(r'エアコン\S*冷房', message.content) is None:
        airconOP(['cool', 27])
        dbUpdate('aircon_mode', 'cool')
        dbUpdate('aircon_temp', 27)
    elif not re.search(r'エアコン\S*暖房', message.content) is None:
        airconOP(['warm', 20])
        dbUpdate('aircon_mode', 'warm')
        dbUpdate('aircon_temp', 20)
    elif not re.search(r'温度\S*下げて', message.content) is None:
        state = getState()
        state[1] -= 1
        airconOP(state)
        dbUpdate('aircon_temp', state[1])
    elif not re.search(r'温度\S*上げて', message.content) is None:
        state = getState()
        state[1] += 1
        airconOP(state)
        dbUpdate('aircon_temp', state[1])
    elif not re.match(r'now', message.content) is None:
        cursor.execute("SELECT * FROM user_data")
        await message.channel.send(str(cursor.fetchall()))

client.run(TOKEN)
```

コードはすべてこちらのリポジトリにあげてあります。

- [ugis70194/smart-remocon](https://github.com/ugis70194/smart-remocon)

## おまけ. Google Assistantと連携してボイスコントロールする

[IFTTT](https://ifttt.com/)を利用してGoogle AssistantとDiscordを連携させます。

![sysytem_c.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/0dd3682b-b898-1c8c-cc75-53da80cff827.jpeg)

まず、[IFTTT](https://ifttt.com/)にアクセスして、メールアドレスを登録します。
`Create`でアプリの作成画面に移ります。
![create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/374ab6e1-923f-5744-f672-048859607bc2.png)

`Add`を押してトリガーとなるアプリを選択します。Google Assistantを選択しましょう。

![choose.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/d1cd9515-3200-abd3-73b2-e5b826e540d8.png)

ここでは`Say a phrase with a text ingredient`を選択します。
![Assistant.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/b7e8d1c9-2810-77ab-bee8-b7d7a2126a83.png)

Google Assistantを利用しているGoogleアカウントと連携します。
![connet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/98028ca5-9a25-1f71-746f-ddc5806f5c7e.png)

呼び出すためのキーフレーズを設定します。ここでは`リモコン`に設定しました。`$`はテキスト変数です。
つまり`リモコン　エアコンつけて`とGoogle Assistantに言うと`リモコンつけて`の部分がDiscordに送信されます。
![set.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/0f68e8ba-87af-4f23-0688-33d96eb21242.png)
Create triggerを押してトリガーを作成します。
`Then That`のAddを押して連携するサービスを選択します。ここではWebhooksを選択しましょう。

![webhooks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/5b065bcf-0d97-268c-bcba-7945aa8f394d.png)

action fieldは以下のように設定してください。URLの部分は後述するDiscordのwebhookURLです。
![Webhooksettings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/be9965bc-1299-6993-118c-15c79df4ebbf.png)

DiscordのWebhookURLを生成します。`サーバー設定 > 連携サービス > ウェブフックの作成`で新しくWebhookURLを作成できます。
![webhoos.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/ae53da60-0fb1-c746-f8fc-1e861ed1d399.png)

名前は何でもいいです。`ウェブフックURLをコピー`を押すとURLがクリップボードにコピーされるので、先程のURL欄にペーストしてください。
![Web.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268516/407bb7ac-8551-e39c-a640-c6f9e3681359.png)

Createを押したらGoogle Assistantとの連携は完了です。
<b>レスポンスがとても遅い(5分〜)ことがあります。テストしたい場合は[Advanced REST client](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo/related?hl=ja)などを利用してリクエストを送ってみてください</b>

#### 参考
- [Discordをゲーム公式Twitterと連携させよう【Discordコミュニティ運用術・機能編8】](https://games.app-liv.jp/archives/467478)
- [Google NestをIFTTTと組み合わせてWEBサービスと連携しよう！
](https://yuki-no-yabo.com/combined-googlehome-with-other-services/)
- [【回避策有】Discord連携でToo many requests to this host – IFTTT](https://kbnt.xyz/4548/)