はじめに
3年前くらいに前、以前住んでいたシェアハウスでこんなシステムを稼働させました。
ここでは以前のシステムについて詳しく触れませんが、AC100Vを使用するセンサーを自作するとかFacebook連携の通知の仕組みなど改良の余地があり、また、大規模な利用を想定したものだったため、もっとパーソナルなものにしようとずっと思っていました。
で、やっと機が熟して(詳しくは触れません!)タイトルにあるように、センサーにスマートプラグを使ったLINEに通知ができるシステムにアップグレードしたものを作ったので公開したいと思います。
稼働状況表示画面
LINE Notify通知画面
システム構成
dataplicity:稼働状況を表示するWebアプリをトンネリング(wormhole)機能を使ってスマホに表示するために利用。
LINE Notify:Webサービスからの通知をLINEで受信するために利用。
スマートプラグ:WiFiでつながるスマートコンセント。Tuyaという会社のプラットフォームを使った製品に対応。
ラズパイ:スマートプラグを監視してLINEに通知するプログラムと、稼働状況を表示するWebアプリが動作。
終了を検知する仕組み
Tuyaプラットフォームを使用したスマートプラグは繋げた機器の消費電力を取得できるようになっていて、その元となる電流や電圧も取れるようになっています。
また、スマートプラグには外部から操作したり状態を取得するAPIが提供されていて、今回はcodetheweb/tuyapiというNode.jsライブラリを使用しています。
ラズパイでcodetheweb/tuyapiを使って電流値を取得し、電流値が閾値より下回ったらスマートプラグに繋げた機器が終了したと判断しています。
事前準備
Smart Lifeアプリで停止時の電流を計る
Tuyaプラットフォームを使った製品は「Smart Life」というスマホアプリで管理できるようになっていて、今回は「Gosund WP6」と「CFMASTER NX-SM300」を使いました。
スマートプラグを「Smart Life」で使えるようにしたら、ランドリールームに行ってスマートプラグに洗濯機や乾燥機を繋げます。(WiFiの電波が届かないと使えませんよ)
そして、機器が停止時の電流値を計ります。わざわざ停止時の電流値を計るのは、停止時も電流が流れる(待機電力を消費する)機器が存在するため、一概に電流が全く流れていない状態を「停止」とは判断できないからです。
WP6では「使用量」アイコンから、NX-SM300では「消費電力」アイコンから「電流値」を見ることができます。
WP6の「使用量」画面(「合計電流」は「現在電流」の間違いです)
NX-SM300の「電気量管理」画面(電圧が115.6Vなのはアメリカ仕様?)
計測した電流値は後で閾値の設定をするときの目安にします。
Tuyaのデベロッパーアカウントを作ってクラウドの設定をする
ここでは「Linking a Tuya device with Smart Link」に書いてある7つの手順の内、4つまでを行っておきます。
でも、その前に、iot.tuya.comにアクセスしてデベロッパーアカウントを作ってくださいね。
- アカウント作成後、上部のナビゲーションバーから「Cloud Development」をクリックして、青い「Create」ボタンをクリックします。
「Create Project」画面に表示される項目は適当に入力して「create」ボタンをクリック後、作られたアプリケーションをクリックして「Access ID/Client ID」と「Access Secret/Client Secret」の値を控えておきます。 - ナビゲーションバーから「App Service」をクリックして、左のメニュー から「 App SDK」をクリックします。
右側にある緑色の「Obtain SDK」ボタンをクリックして、表示された画面から「Wi-Fi device solution」を選び「Next」ボタンをクリックします。
「Enter Infomation」画面では「Channel ID」に「laundryreminder」、「Android Package Name」には「com.xxxx」(xxxxは適当に)と入力して、他の項目は適当に埋めて「Confirm」ボタンをクリックします。 - 「Channel ID」に入力した「laundryreminder」だけ控えておきます。表示されているAppkeyやAppSecretは使用しないので無視します。
- 再びナビゲーションメニューから「Cloud Development」をクリックして、1.で作成したアプリケーションをクリックします。
左のメニューから「Linked Device」をクリックして、「Linked Devices added through app distribution」タブをクリック後、青い「Add Apps」ボタンをクリックします。
「Choose Apps」画面で2.で作ったAppにチェックを付けて「OK」ボタンをクリックします。
このあたりの内容については既にスクリーンショット付きで説明している方がいましたので、よくわからなかった方はそちらをご覧ください。
LINE Notifyの設定
LINE Notifyページにアクセスし、「Webサービスからの通知をLINEで受信」と書いてあるQRコードをスマホで読み込んで友だちに追加します。
LINEアカウントでログインしてマイぺージを開きます。
アクセストークンの発行(開発者向け)から「トークンを発行する」をクリックします。
発行されたトークンを控えておきます。
ラズパイでの作業
dataplicityのインストール
dataplicity.comにアクセスしてアカウントを作ります。
こんな表示があるのでメールアドレスを入力して先を続けてください。
サインインするとこんな画面が表示されます。
表示されているスクリプトをラズパイで実行するとdataplicityがインストールされます。
インストール後に前のページをリロードすると、デバイスにラズパイが表示されます。
ラズパイをクリックすると、右側に緑色のワームホールスイッチがあるのでオンにすると、その下に表示されているURLでインターネットからラズパイにアクセスできるようになります。
重要なのが赤枠で囲った内容で、トンネリングでアクセスできるのはlocalhostへの80番ポートだけなんです。
そのため、ラズパイで動かすWebサーバーは80番ポートを使うようにする必要があります。
Node.jsとPythonの環境整備
ラズパイではスマートプラグを監視してLINEに通知するNode.jsのプログラムと、稼働状況を表示するPythonのWebアプリの2つを動かします。
プログラムはそんなに重くないのでRaspberry Pi Zero Wでも十分動作できますが、インストール済みのモジュールがRaspbian Buster Liteでは少なく環境構築が面倒になるので、CLIで動作させる場合でもRaspbian Buster with desktopを使っています。
構築する環境
Node.jsのインストール
$ sudo apt install -y nodejs npm
Node.jsモジュールのインストール
$ mkdir LaundryReminder2 $ cd LaundryReminder2 $ sudo npm i -g @tuyapi/cli $ npm install codetheweb/tuyapi $ npm install request $ npm install date-utils
Pythonモジュールのインストール
$ sudo pip3 install bottle
スマートプラグのidとlocalKeyの取得
「Linking a Tuya device with Smart Link」に書いてあった7つの手順の残り5~7を行います。
- スマートプラグの電源ボタンを長押し(5秒ぐらい)してリンキングモードにします。このとき、Smart Lifeアプリを立ち上げてデバイスリストからリンキングモードにしたスマートプラグが削除されるのを確認します。
※スマートプラグが複数あるときはひとつづつ5~7を繰り返します。 - ターミナルで以下の引数を指定してtuya-cliコマンドを実行します。
--apikey : クラウドの設定で取得した「Access ID/Client ID」
--api-secret : クラウドで取得した「Access Secret/Client Secret」
--schema : クラウドで設定した「Channel ID」(laundryreminder)
--ssid : スマートプラグに設定するWiFiのSSID
--password : スマートプラグに設定するWiFiのパスワード
--region : us (us, eu, cnから選ぶらしい。jpはない。)
コマンド例:
$ tuya-cli link --api-key "xxxxxxxx" --api-secret "xxxxxxxxxx" --schema "laundryreminder" --ssid "myhome" --password "mypassword" --region us
- 成功すると次のように表示されます。
✔ Device(s) registered!
idとlocalkeyに表示された値を控えておきます。
[
{
id: 'xxxxxxxxxx',
ip: 'xxx.xxx.xxx.xxx',
localKey: 'xxxxxxxx',
name: 'WP6 Mini Smart Plug'
}
]
ファイル構成
ファイル構成はこんな感じになっています。
LaundryReminder2 |- config.json --- 設定ファイル |- sp.js --- スマートプラグ監視プログラム |- web.py --- 稼働状況表示プログラム |- images --- Web用イメージファイルディレクトリ |- off.png --- 停止中アイコン |- on.png --- 稼働中アイコン |- template --- Web用テンプレートディレクトリ |- index.j2 --- テンプレートファイル
config.json(設定ファイル)の設定
スマートプラグが2個の場合
{ "line_token" : "取得したLINEトークンで置き換え", "machine" : [ { "name" : "右側の洗濯機", "id" : "取得したidで置き換え1", "key" : "取得したkeyで置き換え1", "threshold" : 0 }, { "name" : "左側の洗濯機", "id" : "取得したidで置き換え2", "key" : "取得したkeyで置き換え2", "threshold" : 0 } ] }
nameに設定した文字がWebやLINE通知に表示されるので、わかりやすい名前を付けます。
thresholdは閾値の設定です。値の単位はmAです。ここで設定した数値以下になると電源がOFFになったと判断します。
スマートプラグの個数分だけname,id,key,thresholdを増やしてください。
sp.js(スマートプラグ監視プログラム)
TuyAPIを利用してスマートプラグの状態を監視し、電源の状態が変化したらファイルに書き込み、電源がOFFになった場合はLINEに通知します。
プログラムの実行時に設定ファイルのmachine配列の番号を1から指定します。(0だど気持ち悪かったので1からにしました。)
そのため、このプログラムはスマートプラグの個数分実行することになります。
次の記述だと例として挙げたconfig.jsonの「右側の洗濯機」を指定していることになります。
$ node sp.js 1
出力ファイルは指定した番号にjsonの拡張子を付けたファイル名で作成され、内容は次のようになります。
電源がONの場合の例(1.json)
{"state":"on","name":"右側の洗濯機","time":"5/16 07:54"}
電源がOFFの場合の例(1.json)
{"state":"off","name":"右側の洗濯機","time":"5/16 07:54"}
プログラムはTuyAPIの非同期サンプルコードを元に作りました。
/* * スマートプラグ監視プログラム * May 16, 2020 * By Hiroyuki ITO * http://intellectualcuriosity.hatenablog.com/ * MIT Licensed. */ const TuyAPI = require('tuyapi'); const fs = require('fs'); const request = require('request'); require('date-utils'); // コマンドライン引数チェック if (process.argv.length < 2) { console.log('Usage: <number>'); process.exit(-1); } // 設定ファイル読み込み let rawfdata = fs.readFileSync('config.json'); let config = JSON.parse(rawfdata); //console.log(config); let line_token = config.line_token; let name = config.machine[process.argv[2]-1].name; let threshold = config.machine[process.argv[2]-1].threshold; const device = new TuyAPI({ id : config.machine[process.argv[2]-1].id, key: config.machine[process.argv[2]-1].key }); // LINE通知 function lineNotify(message) { const options = { url: 'https://notify-api.line.me/api/notify', headers: { 'Authorization': `Bearer ${line_token}` }, form: { message } }; request.post(options, (error, response, body) => { console.log(body); if (error) { console.error(error); } }); } device.find().then(() => { device.connect(); }); device.on('connected', () => { console.log('Connected to device!'); }); device.on('disconnected', () => { console.log('Disconnected from device.'); }); device.on('error', error => { console.log('Error!', error); }); // スマートプラグに変化があったとき device.on('data', plugdata => { //console.log(plugdata); let data = {}; var mA; var flag = false; if (!(plugdata.dps['4'] === void 0)) { // check undefined mA = plugdata.dps['4']; flag = true; } if (!(plugdata.dps['18'] === void 0)) { // check undefined mA = plugdata.dps['18']; flag = true; } if (flag) { if (mA <= threshold) { // 閾値判定 data['state'] = 'off'; } else { data['state'] = 'on'; } file = process.argv[2]+'.json'; // 状態ファイルの読み込み let status = false, notify = false; try { fs.statSync(file); let rawfdata = fs.readFileSync(file); let fdata = JSON.parse(rawfdata); if (data['state'] != fdata['state']) { // 変化判定 status = true; notify = true; } } catch(err) { status = true; } if (status) { // ON/OFFの変化があった data['name'] = name; let dt = new Date(); data['time'] = dt.toFormat('M/D HH24:MI'); //console.log(data); let wdata = JSON.stringify(data); fs.writeFileSync(process.argv[2]+'.json', wdata); if (data['state'] == 'off' && notify) { lineNotify(`${name}が終わりました。`); // LINE通知 } } } });
web.py(稼働状況表示プログラム)
稼働状況を表示するWebアプリはPythonで作りました。これは個人的にNode.jsよりPythonの方が好きだからです。(Node.jsはバージョンの違いでトラブルに巻き込まれ易いんです。何回かヒドイ目に遭いました。)
スマートプラグ監視プログラムの出力ファイルを個数分読み込んで、状態をリスト表示するだけの簡単なものになっています。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # お洗濯状況 # May 16, 2020 # By Hiroyuki ITO # http://intellectualcuriosity.hatenablog.com/ # MIT Licensed. import json import urllib from bottle import route, run, static_file, auth_basic from bottle import TEMPLATE_PATH, jinja2_template as template # 日本語を使うためのおまじない import io, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') CONFIG_FILE = 'config.json' # 設定ファイル TEMPLATE_PATH.append("./template") # テンプレートパス # ベーシック認証を使うときはコメントを削除して値をセット # USERNAME = "RepraceUsername" # PASSWORD = "RepracePassword" # ベーシック認証 def check(username, password): return username == USERNAME and password == PASSWORD # 機器のON/OFF状態リスト作成 def make_lists(): lists = '' config_open = open(CONFIG_FILE, 'r') config = json.load(config_open) for i in range(len(config['machine'])): state_open = open('%s.json' % str(i+1), 'r') state = json.load(state_open) lists += '<li>\n' lists += ' <img src="../images/%s.png">\n' % state['state'] lists += ' <h1>%s</h1>\n' % state['name'] lists += ' <p>%sより%s</p>\n' % (state['time'], '稼働中' if state['state']=='on' else '停止中') lists += '</li>\n' return lists # ルートへのアクセスのレスポンス @route('/') # @auth_basic(check) # ベーシック認証を使うときはコメントを外す def index(): lists = make_lists() return template('index.j2', lists=lists) # イメージファイルのレスポンス @route('/images/<filename:re:.*\.*>') def send_image(filename): return static_file(filename, root='./images') # 80番ポートを使うときはルート権限が必要 run(host='0.0.0.0', port=80)
off.png、on.png(停止中アイコン、稼働中アイコン)
以下の画像をimagesディレクトリにそれぞれoff.png、on.pngという名前を付けて保存します。
自分でアイコンを作る場合は80x80ピクセルで作ってください。
index.j2(テンプレートファイル)
「Jinja」というテンプレートエンジンのテンプレートファイルです。
中身はほぼHTMLで、26行目にweb.pyで作ったリストを埋め込んでいます。
<!DOCTYPE html> <html> <head> <script type="text/javascript"> function visibilityChangeHandler() { if (!document.hidden) { location.reload(false); } } document.addEventListener('visibilitychange', visibilityChangeHandler, false); </script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>お洗濯状況</title> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css" /> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script> </head> <body> <div data-role="page"> <div data-role="footer" data-theme="b"> <p align='center'>お洗濯状況</p> <button class="ui-btn-right ui-btn ui-corner-all ui-btn-inline ui-mini ui-btn-icon-right ui-icon-refresh" onclick="window.location.reload();">Reload</button> </div> <ul data-role="listview" data-inset="true"> {{lists}} </ul> <div data-role="footer" data-theme="b"> <p align='center'>Created by Hiroyuki ITO</p> <button class="ui-btn-right ui-btn ui-corner-all ui-btn-inline ui-mini ui-btn-icon-right ui-icon-action" onclick="window.open('https://intellectualcuriosity.hatenablog.com/')">Blog</button> </div> </div> </body> </html>
起動時の実行方法
/etc/rc.local
ファイルのexit 0
の前に、次の内容を挿入します。
スマートプラグが2個の場合の例
# Laundry Reminder 2 cd /home/pi/LaundryReminder2 sleep 20 node sp.js 1 & sleep 20 node sp.js 2 & sudo python3 web.py &
Raspberry Pi Zero Wだとsleep 20を入れておかないとsp.jsが動かないことが多かったのですが、Raspberry Pi 3とかなら必要ないかも知れません。
おわり