RaspberryPiでスマートホーム 〜PCの電源をオン・オフする〜
ラズパイでMQTTで指令を受信して、コマンドをリモートから実行できるようにしました。
現在JavascriptとAlexaからMQTTで指令を送信して、照明のオン・オフができます。
今度はPCの電源のオン・オフができるようにしてみます。
PC側
電源オン
PCの電源はラズパイからWake on Lanのマジックパケットを送って、スイッチを入れます。
BIOS
BIOSにそれ用の設定があるので、セットします。マザーボードごとに表記が違うようですが、うちで使ってる”ASRock Z170 Extreme4”の場合は、
”Advanced Mode”→”Advanced”→”ACPI Configuration”の”PCIE Devices Power On”を”Enabled”にしたら、使えるようになります。
あと、Windows側でも設定が必要です。
ネットワークアダプターの設定
”コントロールパネル”→”システム”→”デバイスマネージャー”→”ネットワークアダプター”→”Intel(R)Ethernet Connection (2) I219-V”(環境によって名前に違いあり)を右クリックでプロパティを表示、”電力管理”タブの”Wake on Lan”の項目にチェックが入っていることを確認します。
私の場合はデフォルトで上から3つにチェックが入っていたので何もいじってません。この画面も使ってるアダプターによって変わってきます。
高速スタートアップをオフにする
うちでは高速スタートアップにチェックが入っているとWake On Lanで起動してくれませんでした。
”コントロールパネル”→”電源オプション”から”高速スタートアップを有効にする”のチェックを外します。もしグレーになっててチェックを外せない場合は、上の方にある”現在利用可能ではない設定を変更します”をクリックすると、チェックが外せるようになります。
一度外しても、Windows Updateの時に勝手に復活してたこともありました。
設定すると、PCの電源を落としてもLANの端子のランプがつきっぱなしになってるはずです。
あと、MACアドレスを調べておきます。
コマンドプロンプトで
ipconfig /all
でアダプタ情報が表示されるのでイーサネットアダプターの物理アドレスをコピーしておきます。
電源オフ
Windowsはコマンドプロンプトから以下のコマンドでシャットダウンできます。
shutdown /s /t 5
/t の後ろはシャットダウン開始するまでの秒数です。
スクリプトの作成
上記のシャットダウンコマンドはPC上で実行しないといけないので、電源オフに関してはMQTTでの指令をPC側で受け取ることにします。
ラズパイ用に作成した待ち受けプログラムを少し修正して使用します。
Pythonのスクリプトなので、Pythonがインストールされてないといけません。私はTensorflow用にAnacondaをインストールしてましたので、それを使いました。Anacondaのインストール方法は以下を参考にしてください。
”smarthome”という環境をPython3.6で作成しました。
conda create -n smarthome python=3.6
activateしてから、mqttライブラリの”paho-mqtt”をインストールします。
activate smarthome
pip install paho-mqtt
Pythonのスクリプトhomeapp.pyをラズパイからコピーして修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import paho.mqtt.client import ssl import subprocess import json port = 8883 topic_to = "homeapp/to" topic_from = "homeapp/from" endpoint = "(エンドポイント)" cert = "certificate.pem.crt" key = "private.pem.key" rootCA = "VeriSign.pem" def on_connect(client, userdata, flags, respons_code): client.subscribe(topic_to) def on_message(client, userdata, msg): data = json.loads(msg.payload.decode("utf-8")) if (data["resource"] == "pc"): if (data["command"] == "power" and data["parameters"] == "off"): print("shutting down...") subprocess.call("shutdown /s /t 5") if __name__ == '__main__': client = paho.mqtt.client.Client() client.on_connect = on_connect client.on_message = on_message client.tls_set(rootCA, certfile=cert, keyfile=key, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None) client.connect(endpoint, port=port, keepalive=60) client.loop_forever() |
書き換えたのはon_message()関数で、リソース”pc”、コマンド”power”、パラメータ”off”にのみ反応するようにしました。該当のコマンドを受け取ったら、上記のシャットダウンコマンドを実行します。
接続先(エンドポイント)は、ラズパイで使ってた時と同じです。
なお今回もAWS IoTを使います。なので、ラズパイ同様PCをAWSに登録して、証明書を発行しないといけないのですが…面倒なのでラズパイの証明書をコピーしてPCでも使いました。
証明書は以下の名前でラズパイ側に保存していましたので、それをラズパイからコピーして、スクリプトと同じフォルダに入れます。
- certificate.pem.crt
- private.pem.key
- VeriSign.pem
スタートアップに登録
このスクリプトは常に実行されてないと受信してくれないので、同じフォルダにバッチファイルを作ってスタートアップに登録します。
1 2 |
call activate smarthome python homeapp.py |
homeapp.batという名前で作成しました。
Anacondaで先ほど作ったsmarthome環境をactivateしてから、スクリプトを起動しています。
次にスタートアップフォルダに登録します。
エクスプローラーでこのファイルを右クリックして、”ショートカットの作成(S)”を選び、ショートカットを作成します。普通に起動するとコマンドプロンプトが表示されてしまうので、このショートカットのプロパティを表示させ”ショートカット”タブの”実行時の大きさ”を”最小化にしました。
そして”Windows”キー+”R”でコマンドを”ファイル名を指定して実行”画面を表示させます。”shell:startup”と入力して、エンターキーをおすとスタートアップフォルダが表示されるので、そこに先ほどのショートカットを移動します。
これでWindows起動時にスクリプトが起動するようになりました。起動中はタスクバーにコマンドプロンプトが表示されます。
ラズパイ側
マジックパケット送信
ラズパイからはマジックパケットを送信するのですが、パッケージを利用します。
sudo apt-get install wakeonlan
これで
wakeonlan (MACアドレス)
と打ち込めばマジックパケットが送信できます。
一度ここでWake on LanでちゃんとPCの電源が入るか、実際に実行して確認しておきましょう。なお、先ほどのWindowsのipconfigコマンドでは区切り文字が”-”になってますが、”:”に変えて実行します。
これをシェルスクリプトにしました。
1 2 3 4 |
#!/bin/bash CMD_WOL="/usr/bin/wakeonlan" ${CMD_WOL} $1 |
”wol.sh”という名前で保存しました。待ち受けスクリプトはこのシェルスクリプトを呼び出すことにします。
待ち受けスクリプトの修正
以前作成した、ラズパイ側での待ち受けプログラムを少しだけ改造します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import paho.mqtt.client import ssl import subprocess import json port = 8883 topic_to = "homeapp/to" topic_from = "homeapp/from" endpoint = "(エンドポイント)" cert = "certificate.pem.crt" key = "private.pem.key" rootCA = "VeriSign.pem" macaddr = {"pc":"xx:xx:xx:xx:xx:xx"} def on_connect(client, userdata, flags, respons_code): client.subscribe(topic_to) def on_message(client, userdata, msg): data = json.loads(msg.payload.decode("utf-8")) if (data["resource"] == "pc"): if (data["command"] == "power" and data["parameters"] == "on"): subprocess.call("/bin/bash wol.sh " + macaddr[data["resource"]], shell=True) else: par = data["resource"] + "_" + data["command"] + ("_" + data["parameters"] if data["parameters"] else "") subprocess.call("/bin/bash ir.sh " + par, shell=True) if __name__ == '__main__': client = paho.mqtt.client.Client() client.on_connect = on_connect client.on_message = on_message client.tls_set(rootCA, certfile=cert, keyfile=key, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None) client.connect(endpoint, port=port, keepalive=60) client.loop_forever() |
on_message()関数で、リソース”pc”に対応するようにしました。同じフォルダにある”wol.sh”をMACアドレスを引数に呼び出します。
MACアドレスはソースの先頭の”xx:xx:xx:xx:xx:xx:”を実際のものに置き換えます。
送信側
送信側も”pc”というリソースを送れるようにします。
Javascript
こちらはHTMLに行を増やすだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js" type="text/javascript"></script> <script src="https://sdk.amazonaws.com/js/aws-sdk-2.171.0.min.js" type="text/javascript"></script> <script src="SigV4Utils.js" type="text/javascript"></script> <script src="homeapp.js" type="text/javascript"></script> </head> <body> <input type="hidden" id="topic" value="homeapp/to"> 【照明】<br> 電源: <input type="radio" name="light_power" value="on" data-resource="light" data-command="power" onclick="javascript:send(this)">オフ <input type="radio" name="light_power" value="off" data-resource="light" data-command="power" onclick="javascript:send(this)">オフ <br> 【PC】<br> 電源: <input type="radio" name="pc_power" value="on" data-resource="pc" data-command="power" onclick="javascript:send(this)">オン <input type="radio" name="pc_power" value="off" data-resource="pc" data-command="power" onclick="javascript:send(this)">オフ <br> </body> </head> |
【PC】以下の5行を追加してます。
Alexa(スマートホームスキル)
AWSのLambdaのソースを少し変更します。
Discovery用のデータを増やすことと、endpointの判断ロジックが追加されたところが変更点です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
const AWS = require('aws-sdk'); const ENDPOINT = "(エンドポイント)"; exports.handler = function (request, context) { switch (request.directive.header.namespace) { case "Alexa.Discovery": if (request.directive.header.name === 'Discover') { handleDiscovery(request, context, ""); } break; case "Alexa.PowerController": handlePowerControl(request, context); break } function handleDiscovery(request, context) { var payload = { "endpoints": [ { "endpointId": "LIGHT-001", "manufacturerName": "my-scribble.net", "friendlyName": "照明", "description": "部屋の照明", "displayCategories": ["LIGHT"], "cookie": {}, "capabilities": [ { "type": "AlexaInterface", "interface": "Alexa.PowerController", "version": "3", "properties": { "supported": [{ "name": "powerState" }], "retrievable": true } } ] }, { "endpointId": "LIGHT-001", "manufacturerName": "my-scribble.net", "friendlyName": "電気", "description": "部屋の照明", "displayCategories": ["LIGHT"], "cookie": {}, "capabilities": [ { "type": "AlexaInterface", "interface": "Alexa.PowerController", "version": "3", "properties": { "supported": [{ "name": "powerState" }], "retrievable": true } } ] }, { "endpointId": "PC-001", "manufacturerName": "my-scribble.net", "friendlyName": "パソコン", "description": "部屋のパソコン", "displayCategories": ["OTHER"], "cookie": {}, "capabilities": [ { "type": "AlexaInterface", "interface": "Alexa.PowerController", "version": "3", "properties": { "supported": [{ "name": "powerState" }], "retrievable": true } } ] }, ] }; var header = request.directive.header; header.name = "Discover.Response"; context.succeed({ event: { header: header, payload: payload } }); } function handlePowerControl(request, context) { var requestMethod = request.directive.header.name; var endPoint = request.directive.endpoint.endpointId; var resource = ""; var command = ""; var option = ""; var responseNamespace = ""; var responseName = ""; var responseResult = ""; switch (endPoint) { case "LIGHT-001": resource = "light"; break; case "PC-001": resource = "pc"; break; } switch (requestMethod) { case "TurnOn": command = "power"; option = "on"; responseNamespace = "Alexa.PowerController"; responseName = "powerState"; responseResult = "ON"; break; case "TurnOff": command = "power"; option = "off"; responseNamespace = "Alexa.PowerController"; responseName = "powerState"; responseResult = "OFF"; break; } if (resource && command && option) { submit(resource, command, option, function(){ sendResponse(responseNamespace, responseName, responseResult); }); } } function sendResponse(namespace, name, value, payload) { var contextResult = { "properties": [{ "namespace": namespace, "name": name, "value": value, "timeOfSample": "2017-09-03T16:20:50.52Z", //retrieve from result. "uncertaintyInMilliseconds": 50 }] }; var responseHeader = request.directive.header; responseHeader.namespace = "Alexa"; responseHeader.name = "Response"; responseHeader.messageId = responseHeader.messageId + "-R"; var response = { context: contextResult, event: { header: responseHeader }, payload: payload }; context.succeed(response); } function submit(resource, command, params, callback) { var data = { topic: 'homeapp/to', payload: JSON.stringify({ "resource": resource, "command": command, "parameters": params, }), qos: 0 }; var iotdata = new AWS.IotData({endpoint: ENDPOINT}); iotdata.publish(data, function(err, data){ if(err){ console.log("submit(): failure - " + data); } else{ console.log("submit(): success"); } if (callback) { callback(); } }); } }; |
実行してみる
実際に実行して確認してみます。うまくいかないときは…
Wake on Lanが動作するか
まずはMQTT云々以前に、ラズパイから手でコマンド叩いて、PCが起動するか確認です。MACアドレスはちゃんと有線LANのアドレスか確認します。
データが送信されているか
AWSのIoT Coreのテスト画面で、トピックをサブスクライブしてデータが送信されているか確認します。
以下のページの”テスト”を参考にしてください。
ラズパイが受信しているか
on_message()関数内で、print(data)でもしてみます。
nohup python homeapp.py &
で起動しているのなら、nohup.txtにprintの内容が表示されるのでちゃんと受信しているか確認します。
PCが受信しているか
こちらも同じく、on_message()関数内で、print(data)でもしてみます。
タスクバー上のコマンドプロンプトをクリックすると画面が開くので、受信していればそこに表示されるはずです。
感想
これで声でPCのオン・オフができるようになりました。PCのところに行く前にアレクサに電源入れてもらえば、椅子に座るころには電源が入って、起動が完了しています!
さらに今までは電源切り忘れたままベッドに入った時は、消すの面倒くさくてつけっぱなしで寝てましたが、もう大丈夫!
まあ、PCはめっちゃ近くにあるんですけどね。なんか、どんどんダメ人間になっていっている気がするのは、気のせいでしょうか…
さらに…
ラズパイとは関係ないですけど、Windows側では待ち受けのスクリプトが走っているので、さらにコマンドを増やしてやれば電源オンオフだけでなく色々できそうですね。例えば”startapp”というコマンドでアプリを起動したり…
アレクサ使えば声で起動できます。ただスマートホームスキルでは使える語呂が少なさそうなので、この場合はカスタムスキルの方がよさそうですね。
夢は広がります。