はじめに
自宅のエアコンをLINEから操作するようなシステムを作っていきます.
記事が長くなりそうなので
- ラズパイからエアコンを操作できるようにする
- LINEからメッセージを送ってエアコンを操作できるようにする
の2つに分けます.適宜必要な箇所を参照していただけると幸いです.
この記事では,2のLINEからメッセージを送り,それを受けてエアコンを制御する部分について説明します.
1のハードウェア部分の構成に関してはこちらです.
環境はWindows10, WSLで行います.
完成形
こんな感じでLINEからエアコンを操作します(クリックするとyoutubeに飛びます).
システムのフロー図を次に示します.
操作はLINEから行うことができるので,出先のスマホからや研究室のPCなどから操作できます.
LINE APIからはWebhookが飛ばされ,それをHeroku上に立てたwebサーバがキャッチし,mqttを通してラズパイに知らせます.
ラズパイでは,赤外LEDを制御し,あらかじめ記録しておいたエアコンの信号を再生することでエアコンを制御するという形になっています.
LINEの画面はこのようになっていて,リッチメニューで操作できます.
また,センサを使用してエアコンが点灯したかどうかを確認できるようになっています.
本編の概要
本編は,
- LINE Botの作成
- HerokuとBeeboteの準備
- Heroku上にWebサーバをデプロイする
- ラズパイ上で動作させるプログラムの説明
- 動作確認
- 付録
という構成になっています.
LINE Botの作成
LINEのMessaging APIを使用して今回のシステムで操作の受け付け・結果表示を行うLINEのボットを作成します.
こちらのサイトを参考にして,LINE Developersへの登録・チャネルの作成を行ってください.
「aircon_control」と適当に名前をつけてこのようにボットが作成できたら完了です.
次に友達登録を行います.チャネルの管理画面から,Messaging APIをクリックし,QR codeを使用して友達登録を行ってください.
また次の画面を参考に「Auto-reply message」と「Greeting message」をDisabledに設定し,「Issue」ボタンを押してチャネルアクセストークンを発行します.
今後必要になってくる情報は以下の2つです.
- Channel secret
- Channel access token
こちらの情報は,先程のチャネルの管理画面から参照することができるので,必要になったらこのページにきてコピーしてください.
HerokuとBeeboteの準備
Herokuのセットアップ
Heroku とは
こちらのサイトがわかりやすいです.
HerokuとはPaaSと呼ばれるWEBアプリケーションを簡単にホスティングできるサービスです.
初めからネットワークなどのインフラやOSなどのプラットフォームが整っている状態なので,ユーザはソースを書いてアップロードするだけで勝手にデプロイしてくれます.
登録
まずこちらからHerokuの登録を行ってください.
Heroku CLIのインストール
今回はWSL上にHeroku CLI(コマンドラインからHerokuを操作するもの)をインストールしたいと思います.
WSLのインストールはこちらを参照してください.バージョン1で大丈夫です.
Heroku CLIのインストールはこちらのサイトを参考にしました.
下記コマンドをWSL上で実行してください.
$ curl https://cli-assets.heroku.com/install.sh | sh
次のコマンドでHerokuにログインできたら成功です(ブラウザが立ち上がってログインするとターミナル上でもログインが完了します).
$ heroku login
アプリの登録
適当にアプリケーションの名前をつけてアプリを作成します.
この名前がURLに組み込まれるので,他の人と被ると使えません.
なので汎用的な名前(test
など)はもう使われているので適当に探しましょう.また_(アンダーバー)
は使えないので-(ハイフン)
で代替します.
$ heroku create {アプリ名}
あるいはHerokuの管理画面からもアプリの作成が行なえます.
Beebotteのセットアップ
登録
こちらのサイトへ行ってBeebotteへの登録を行います.
新規チャネルの作成
登録が終わったら管理画面に行き,新規チャネルの登録を行います.
Create Newをクリックします.
次の画面で名前等を設定します.
今回は,
設定 | 値 |
---|---|
Channel Name | my_home |
Resource name | aircon_control |
と設定しました.ここは何でも大丈夫です.
ここの値がmqttのトピック名(my_home/aircon_control)のように設定されます.
トークンの取得
管理画面から先程作成したChannelをクリックするとChannel Token(下図のtoken_*の部分)を得ることが出来ます.
Webサーバをデプロイする
LINEのMessaging APIから送信されるwebhook
を受け取るwebサーバを作成します.
環境変数を設定する
まずトークンなどをHerokuの内部の環境変数として定義します.
トークンの悪用などを防ぐ目的なので必ず行いましょう.
まずLINE API関係です.
$ heroku config:set YOUR_CHANNEL_SECRET="Channel Secretの文字列" --app {アプリ名}
$ heroku config:set YOUR_CHANNEL_ACCESS_TOKEN="Access tokenの文字列" --app {アプリ名}
Channel Secret
とAccess token
に関しては先程のLINEBotの管理画面から参照してください.
次にBeebotte関係です.
$ heroku config:set YOUR_BEEBOTTE_TOKEN="tokenの文字列(token_*の形)" --app {アプリ名}
先程取得したトークンを入力します.
必要なファイルを用意する
必要なファイルはこの5つです.
ファイル名 | 役割 |
---|---|
main.py | ソースコード |
runtime.txt | Pythonのバージョンを記載 |
requirements.txt | インストールするライブラリとそのバージョンを記載 |
Procfile | プログラムの実行方法を定義 |
mqtt.beebotte.com.pem | Beebotteのサーバにアクセスするための証明書.こちらからダウンロード. |
適当にディレクトリを作成し,以下の内容をコピーしてファイルを作成してください.
ファイル名を間違えると怒られるので気をつけてください.
python-3.7.0
Flask==1.0.2
line-bot-sdk==1.16.0
paho-mqtt==1.5.0
web: python main.py
main.py
に関しては少し長いので次をクリックして見てください.
クリックして展開
from flask import Flask, request, abort
import paho.mqtt.publish as publish
import os
import json
from linebot import (
LineBotApi, WebhookHandler
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage,
)
app = Flask(__name__)
# LINE API関係の設定値取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ['YOUR_CHANNEL_ACCESS_TOKEN']
YOUR_CHANNEL_SECRET = os.environ['YOUR_CHANNEL_SECRET']
# Beebotte関係の設定値取得
YOUR_BEEBOTTE_TOKEN = os.environ['YOUR_BEEBOTTE_TOKEN']
line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)
# 動作を起こすメッセージのリスト
on_msg = [s.encode('utf-8') for s in ['on', 'エアコンつけて!']]
off_msg = [s.encode('utf-8') for s in ['off', 'エアコン切って!']]
# LINEに通知メッセージを送る
def broadcast_line_msg(msg):
line_bot_api.broadcast(TextSendMessage(text=msg))
# エアコン制御用のMQTTをパブリッシュする
def publish_aircon_control_msg(msg):
publish.single('my_home/aircon_control', \
msg, \
hostname='mqtt.beebotte.com', \
port=8883, \
auth = {'username':'token:{}'.format(YOUR_BEEBOTTE_TOKEN)}, \
tls={'ca_certs':'mqtt.beebotte.com.pem'})
@app.route('/callback', methods=['POST'])
def callback():
# get X-Line-Signature header value
signature = request.headers['X-Line-Signature']
# get request body as text
body = request.get_data(as_text=True)
app.logger.info('Request body: ' + body)
# handle webhook body
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
msg = event.message.text.encode('utf-8')
if msg in on_msg:
publish_aircon_control_msg('on')
elif msg in off_msg:
publish_aircon_control_msg('off')
else:
broadcast_line_msg('\n'.join(['エアコンをつけたい時:', \
*['['+s.decode('utf-8')+']' for s in on_msg], \
'\nエアコンを消したい時:', \
*['['+s.decode('utf-8')+']' for s in off_msg] , \
'\nって話しかけてね!']))
if __name__ == '__main__':
port = int(os.getenv('PORT'))
app.run(host='0.0.0.0', port=port)
main.pyの説明
以下,main.pyの簡単な説明です.
# LINE API関係の設定
YOUR_CHANNEL_ACCESS_TOKEN = os.environ['YOUR_CHANNEL_ACCESS_TOKEN']
YOUR_CHANNEL_SECRET = os.environ['YOUR_CHANNEL_SECRET']
# Beebotte関係の設定
YOUR_BEEBOTTE_TOKEN = os.environ['YOUR_BEEBOTTE_TOKEN']
先程環境変数として設定したLINE APIやBeebotteのトークンを取得しています
# 動作を起こすメッセージのリスト
on_msg = [s.encode('utf-8') for s in ['on', 'エアコンつけて!']]
off_msg = [s.encode('utf-8') for s in ['off', 'エアコン切って!']]
LINEから受け取ったメッセージのうち,どのフレーズでon/off動作を行うか設定します.
日本語の処理が面倒なので,一旦すべてUTF-8にエンコードして扱います.
# エアコン制御用のMQTTをパブリッシュする
def publish_aircon_control_msg(msg):
publish.single('my_home/aircon_control', \
msg, \
hostname='mqtt.beebotte.com', \
port=8883, \
auth = {'username':'token:{}'.format(YOUR_BEEBOTTE_TOKEN)}, \
tls={'ca_certs':'mqtt.beebotte.com.pem'})
publish_aircon_control_msg
関数ではmsg
を受け取ってそれをpayloadとしてパブリッシュします.
mqttのパブリッシュにはpaho.mqttののPublishクラスを使用します.リファレンスはこちらです.
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
msg = event.message.text.encode('utf-8')
if msg in on_msg:
publish_aircon_control_msg('on')
elif msg in off_msg:
publish_aircon_control_msg('off')
else:
broadcast_line_msg('\n'.join(['エアコンをつけたい時:', \
*['['+s.decode('utf-8')+']' for s in on_msg], \
'\nエアコンを消したい時:', \
*['['+s.decode('utf-8')+']' for s in off_msg] , \
'\nって話しかけてね!']))
LINEからのwebhookの署名が正しい場合にこちらのハンドラが実行されます.
on/offメッセージを判断し,mqttをパブリッシュします.それ以外のメッセージの場合,反応するメッセージのリストを示すようになっています.
表示はこんな感じです.
デプロイを行う
HerokuではアプリのソースをGitレポジトリとして管理するので,ローカルでGitレポジトリを構成します.
対象のディレクトリで移動し,次のコマンドを実行します.
$ git init
ここで
*** Please tell me who you are.
Run
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
to set your account's default identity.
Omit --global to set the identity only in this repository.
などと出たらgitの初期設定を行っていないので指示に従って設定を済ませます.
リモートレポジトリにherokuのレポジトリを指定します.
$ heroku git:remote -a {アプリ名}
次に各ファイルを追跡対象に加えコミットします.
$ git add .
$ git commit -m "Initial commit"
最後にプッシュします.こちらはHerokuにログインした状態で行ってください.
$ git push heroku master
これでエラーログなどが出なければデプロイ成功です.
LINE APIでWebhookの設定を行う
LINE APIの管理画面からWebhookの設定を行います.
Webhook URLに,
https://{アプリ名}.herokuapp.com/callback
と設定してVerifyします.
以上でLINE->Heroku->Beebotteの通路が完成しました.
ラズパイ上で動作させるプログラム
次にPublishされたmqttをSubscribeして赤外LEDを制御するラズパイ側のプログラムを説明します.
ライブラリのインストール
まずpip3
からインストールします.Raspbian
では最初から入っていないため,手動でインストールする必要があります.
こちらのサイトを参考にしました.
$ sudo apt-get -y install python3-dev
$ sudo apt-get -y install python3-pip
インストールされたか確認します.
$ pip3 --version
バージョン情報が出てきたら成功です.
次に必要なライブラリをインストールします.
$ pip3 install paho-mqtt
$ pip3 install line-bot-sdk
証明書のダウンロード
こちらからmqtt.beebotte.com.pem
をダウンロードして,同ディレクトリに置いてください.
プログラム本体
クリックして展開
import subprocess
import paho.mqtt.client as mqtt # MQTTのライブラリをインポート
import datetime
from linebot import LineBotApi
from linebot.models import TextSendMessage
from linebot.exceptions import LineBotApiError
# LINEでメッセージを送信
def send_line_msg(msg):
line_bot_api = LineBotApi({LINEのアクセストークンの文字列})
line_bot_api.broadcast(TextSendMessage(text=msg))
# ブローカーに接続できたときの処理
def on_connect(client, userdata, flag, rc):
print('[mqtt_aircon_control.py] Connected with result code ' + str(rc)) # 接続できた旨表示
client.subscribe('my_home/aircon_control') # subするトピックを設定
# ブローカーが切断したときの処理
def on_disconnect(client, userdata, flag, rc):
if rc != 0:
print('[mqtt_aircon_control.py] Unexpected disconnection.')
# メッセージが届いたときの処理
def on_message(client, userdata, msg):
# エアコンの信号データのパスを指定
stop_log_path = '/home/pi/ws/send_data/stop.log'
heater_log_path = '/home/pi/ws/send_data/heater.log'
cooler_log_path = '/home/pi/ws/send_data/cooler.log'
# プログラムの実行ファイルのパスを指定
exec_send_command = '/home/pi/ws/send'
exec_switching_verify_command = '/home/pi/ws/switching_verify'
# メッセージ受け取り
get_msg = msg.payload.decode('utf-8')
# 現在日時を取得し,送信するデータファイルを決定する
dt_now = datetime.datetime.now()
print('[mqtt_aircon_control.py] Get aircon_control topic(msg:{}) [{}]'.format(get_msg, dt_now.strftime('%Y/%m/%d %H:%M:%S')))
if 11 <= int(dt_now.strftime('%m')) or int(dt_now.strftime('%m')) <= 4:
send_data_file = heater_log_path
else:
send_data_file = cooler_log_path
# 信号送信処理
if get_msg == 'on':
send_line_msg('エアコンの電源を入れるよ!')
for i in range(3):
subprocess.run(['sudo', exec_send_command, send_data_file])
ret = subprocess.run([exec_switching_verify_command , '10'])
try:
if ret.returncode == 0:
print('[mqtt_aircon_control.py] Switching successed.')
send_line_msg('エアコンの起動を確認したよ!')
break
except:
print('[mqtt_aircon_control.py] Switching failed.')
continue
elif get_msg == 'off':
send_line_msg('エアコンの電源を消すよ!')
subprocess.run(['sudo', exec_send_command, stop_log_path])
# MQTTの接続設定
client = mqtt.Client() # クラスのインスタンス(実体)の作成
client.on_connect = on_connect # 接続時のコールバック関数を登録
client.on_disconnect = on_disconnect # 切断時のコールバックを登録
client.on_message = on_message # メッセージ到着時のコールバック
client.username_pw_set("token:{Beebotteのチャネルトークン(token_*の形)}")
client.tls_set("/home/pi/ws/mqtt.beebotte.com.pem")
client.connect("mqtt.beebotte.com", 8883, 60)
send_line_msg('システムを起動するよ!')
client.loop_forever() # 永久ループして待ち続ける
プログラムの説明
上記のプログラムの簡単な説明を行います.
# LINEでメッセージを送信
def send_line_msg(msg):
line_bot_api = LineBotApi({LINEのアクセストークンの文字列})
line_bot_api.broadcast(TextSendMessage(text=msg))
send_line_msg
関数では,引数に文字列を受け取り,それをLINEの登録している友達に送信します.{LINEのアクセストークンの文字列}には自分のLINEBotのアクセストークンを記入します.
# メッセージが届いたときの処理
def on_message(client, userdata, msg):
# エアコンの信号データのパスを指定
stop_log_path = '/home/pi/ws/send_data/stop.log'
heater_log_path = '/home/pi/ws/send_data/heater.log'
cooler_log_path = '/home/pi/ws/send_data/cooler.log'
# プログラムの実行ファイルのパスを指定
exec_send_path = '/home/pi/ws/send'
exec_switching_verify_path = '/home/pi/ws/switching_verify'
# メッセージ受け取り
get_msg = msg.payload.decode('utf-8')
# 現在日時を取得し,送信するデータファイルを決定する
dt_now = datetime.datetime.now()
print('[mqtt_aircon_control.py] Get aircon_control topic(msg:{}) [{}]'.format(get_msg, dt_now.strftime('%Y/%m/%d %H:%M:%S')))
if 11 <= int(dt_now.strftime('%m')) or int(dt_now.strftime('%m')) <= 5:
send_data_file = heater_log_path
else:
send_data_file = cooler_log_path
# 信号送信処理
if get_msg == 'on':
send_line_msg('エアコンの電源を入れるよ!')
for i in range(3):
subprocess.run(['sudo', exec_send_path, send_data_file])
ret = subprocess.run([exec_switching_verify_path , '10'])
try:
if ret.returncode == 0:
print('[mqtt_aircon_control.py] Switching successed.')
send_line_msg('エアコンの起動を確認したよ!')
break
except:
print('[mqtt_aircon_control.py] Switching failed.')
continue
elif get_msg == 'off':
send_line_msg('エアコンの電源を消すよ!')
subprocess.run(['sudo', exec_send_path, stop_log_path])
on_message
関数は,paho
のmqttクライアントがサブスクライブしているトピックのメッセージを受信した際に実行される関数です.
処理内容は,まずメッセージを受け取りon
とoff
を判別します.
on
の場合,11月から4月なら暖房,それ以外なら冷房の信号を送信します.
off
の場合は,そのままstop
信号を送信します.
ユーザが環境によって変えるところは,信号のログファイル(ハードウェア編で作成したstop.log
など)と,送信プログラムと起動確認用プログラムの実行ファイルのパスです.
# MQTTの接続設定
client = mqtt.Client() # クラスのインスタンス(実体)の作成
client.on_connect = on_connect # 接続時のコールバック関数を登録
client.on_disconnect = on_disconnect # 切断時のコールバックを登録
client.on_message = on_message # メッセージ到着時のコールバック
client.username_pw_set("token:{Beebotteのチャネルトークン(token_*の形)}")
client.tls_set("/home/pi/ws/mqtt.beebotte.com.pem")
client.connect("mqtt.beebotte.com", 8883, 60)
send_line_msg('システムを起動するよ!')
client.loop_forever() # 永久ループして待ち続ける
main
関数がないので,スクリプトを実行したらこのコードが実行されます.
client.tls_set
にはサーバ証明書(mqtt.beebotte.com.pem
)のフルパスを指定します.
電源を入れた時にプログラムを自動起動
電源を入れる度に先程のpyファイルを実行するのは面倒くさいので,ラズパイが起動したら際にプログラムも自動で起動するようにします.
自動起動にはいくつか方法がありますが今回はsystemdを使用します.
こちらの記事を参考にしました.
*.service
に指定する実行するシェルスクリプトだけ示します(記事中のautoexec.sh
に該当するもの).
#!/bin/sh
/usr/bin/python3 -u /home/pi/ws/mqtt_aircon_control.py >> /home/pi/ws/aircon_control.log 2>&1
/usr/bin/python3
はフルパスを指定する必要があります.
-u
は標準出力のバッファリングを無効にします.実行中の出力などをファイルに書き出したいため,このオプションを指定します.
/home/pi/ws/mqtt_aircon_control.py
は先程作成したpythonスクリプトです.
>> /home/pi/ws/aircon_control.log 2>&1
はスクリプトの出力をファイルにリダイレクトします.ここではpythonスクリプトと同ディレクトリに追記で出力するようにしています.
動作確認&最後に
以上で作業は完了になります.
on
やoff
とメッセージを送ることでラズパイが動作し,エアコンが制御できるようになりましたか?
上手くいかない場合はデバッグを行うことになります.
以下のコマンドでherokuのログを確認したり,デバッグプリントを入れたりして地道に原因を突き止めましょう.
$ heroku logs --tail
デバッグは辛いときも多いですがその分動作したときの喜びも一入です.
頑張ってください.
なにか分からないことがあればコメントで対応します.
ちなみに当初はCloudMQTTを使用してこちらの記事を書いていました.
ですが,無料プランが使用できなくなるとのことで急遽Beebotteを使う仕様に変更しました.なので,Beebotteならこうした方がいいよ等あるかもしれませんがその時ははコメントで教えて下さい.
CloudMQTTの方が手軽だったので残念でした.
最後に,ハードウェア編もよろしくお願いします.
付録A.LINE botにリッチメニューを実装する
LINE APIを使用してリッチメニューから操作できるようにします.
Messaging APIのリッチメニューに関してはこちらを参照してください.
リッチメニューを登録する順番は,
- リッチメニューオブジェクトをボディにしたリクエストをだし,richMenuIdを取得する.
- 画像をアップロードする
- デフォルトのリッチメニューとして登録する
という感じです.これを自動で行うpythonスクリプトを作成したのでそれを利用してください.
まず,画像編集ソフトを使用してリッチメニューにアップする画像を作成します.
自分が作成した画像も一応上げておきます.
次に下記のスクリプトを使用してリッチメニューの登録を行います.
クリックして展開
import requests
url = 'https://api.line.me/v2/bot/richmenu'
access_token={channel access token}
img_path = 'rich_menu_simple.png'
send_data='''{
"size":{
"width":2500,
"height":843
},
"selected": true,
"name": "LINE Developers Info",
"chatBarText": "Tap to open",
"areas": [
{
"bounds": {
"x": 0,
"y": 0,
"width": 1250,
"height": 843
},
"action": {
"type": "message",
"label": "on",
"text": "エアコンつけて!"
}
},
{
"bounds": {
"x": 1250,
"y": 0,
"width": 1250,
"height": 843
},
"action": {
"type": "message",
"label": "off",
"text": "エアコン切って!"
}
}
]
}'''
header = {'Authorization': 'Bearer {}'.format(access_token)}
res = requests.post(url, headers={**header, **{'Content-Type': 'application/json'}}, data=send_data.encode("utf-8"), verify=True).json()
update_url = 'https://api.line.me/v2/bot/richmenu/{}/content'.format(res['richMenuId'])
img_file = open(img_path, "rb")
update_res = requests.post(update_url, headers={**header, **{'Content-Type': 'image/jpeg'}}, data=img_file, verify=True).json()
if update_res:
print('[ERROR] ', update_res)
exit()
apply_url = 'https://api.line.me/v2/bot/user/all/richmenu/{}'.format(res['richMenuId'])
apply_res = requests.post(apply_url, headers=header, verify=True).json()
if apply_res:
print('[ERROR] ', apply_res)
exit()
print('[SUCCESS] Rich menu image update!')
使用する場合はaccess_token
,img_path
,send_data
を編集します.
send_data
に関しては先程のリンクを参考にしてください.
実行してSUCCESSが表示されたら成功です.
リッチメニューを押すことで特定のメッセージが送信される仕組みになっています(その後はいままでの処理と同じ).
付録B.Heroku上のアプリを24時間起動させる
通常,HerokuにデプロイしたWebアプリは30分間アクセスがないと自動で停止し,次にアクセスが起こった時にまた起動します.
この起動に時間がかかるため,できるだけ起動したままの状態を保持したいです.
今回はcronを使用して定期的にサーバにリクエストを送ることで停止するのを防ぎます.
以下のコマンドで設定します.
$ crontab -e
エディタが開いたらファイルの最後に以下を追記します.
20分ごとにリクエストをだし,出力は捨てるようになっています.
*/20 * * * * curl https://{アプリ名}.herokuapp.com/ > /dev/null 2>&1
正常に動作しているかの確認はherokuのログを見ることで確認できます.
次のコマンドでログを見てみましょう.
$ heroku logs --tail
Herokuの無料プランの枠は一月あたり1000時間なので,1つのアプリを運用することは問題ありあませんが,2つ以上はこれを超過してしまうため気をつけましょう.