知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

スマートプラグ(コンセント)とラズパイで洗濯機や乾燥機の終了をLINEに通知できて稼働状況もわかるシステムを作った

はじめに

3年前くらいに前、以前住んでいたシェアハウスでこんなシステムを稼働させました。

ここでは以前のシステムについて詳しく触れませんが、AC100Vを使用するセンサーを自作するとかFacebook連携の通知の仕組みなど改良の余地があり、また、大規模な利用を想定したものだったため、もっとパーソナルなものにしようとずっと思っていました。

 

で、やっと機が熟して(詳しくは触れません!)タイトルにあるように、センサーにスマートプラグを使ったLINEに通知ができるシステムにアップグレードしたものを作ったので公開したいと思います。

 

稼働状況表示画面

f:id:IntellectualCuriosity:20200510055310j:plain

 

LINE Notify通知画面

f:id:IntellectualCuriosity:20200510055357j:plain

 

システム構成

f:id:IntellectualCuriosity:20200510060338p:plain
dataplicity
:稼働状況を表示するWebアプリをトンネリング(wormhole)機能を使ってスマホに表示するために利用。

LINE NotifyWebサービスからの通知をLINEで受信するために利用。

スマートプラグWiFiでつながるスマートコンセント。Tuyaという会社のプラットフォームを使った製品に対応。

ラズパイ:スマートプラグを監視してLINEに通知するプログラムと、稼働状況を表示するWebアプリが動作。

 

終了を検知する仕組み

f:id:IntellectualCuriosity:20200510201959p:plain

Tuyaプラットフォームを使用したスマートプラグは繋げた機器の消費電力を取得できるようになっていて、その元となる電流や電圧も取れるようになっています。

また、スマートプラグには外部から操作したり状態を取得するAPIが提供されていて、今回はcodetheweb/tuyapiというNode.jsライブラリを使用しています。

ラズパイでcodetheweb/tuyapiを使って電流値を取得し、電流値が閾値より下回ったらスマートプラグに繋げた機器が終了したと判断しています。

 

事前準備
Smart Lifeアプリで停止時の電流を計る

f:id:IntellectualCuriosity:20200511144057j:plain

Tuyaプラットフォームを使った製品は「Smart Life」というスマホアプリで管理できるようになっていて、今回は「Gosund WP6」と「CFMASTER NX-SM300」を使いました。

スマートプラグを「Smart Life」で使えるようにしたら、ランドリールームに行ってスマートプラグに洗濯機や乾燥機を繋げます。(WiFiの電波が届かないと使えませんよ)

そして、機器が停止時の電流値を計ります。わざわざ停止時の電流値を計るのは、停止時も電流が流れる(待機電力を消費する)機器が存在するため、一概に電流が全く流れていない状態を「停止」とは判断できないからです。

WP6では「使用量」アイコンから、NX-SM300では「消費電力」アイコンから「電流値」を見ることができます。

WP6の「使用量」画面(「合計電流」は「現在電流」の間違いです)

f:id:IntellectualCuriosity:20200511150823j:plain

NX-SM300の「電気量管理」画面(電圧が115.6Vなのはアメリカ仕様?)

f:id:IntellectualCuriosity:20200511151000j:plain

計測した電流値は後で閾値の設定をするときの目安にします。

 

Tuyaのデベロッパーアカウントを作ってクラウドの設定をする

ここでは「Linking a Tuya device with Smart Link」に書いてある7つの手順の内、4つまでを行っておきます。

でも、その前に、iot.tuya.comにアクセスしてデベロッパーアカウントを作ってくださいね。

  1. アカウント作成後、上部のナビゲーションバーから「Cloud Development」をクリックして、青い「Create」ボタンをクリックします。
    「Create Project」画面に表示される項目は適当に入力して「create」ボタンをクリック後、作られたアプリケーションをクリックして「Access ID/Client ID」と「Access Secret/Client Secret」の値を控えておきます。
  2. ナビゲーションバーから「App Service」をクリックして、左のメニュー から「 App SDK」をクリックします。
    右側にある緑色の「Obtain SDK」ボタンをクリックして、表示された画面から「Wi-Fi device solution」を選び「Next」ボタンをクリックします。
    「Enter Infomation」画面では「Channel ID」に「laundryreminder」、「Android Package Name」には「com.xxxx」(xxxxは適当に)と入力して、他の項目は適当に埋めて「Confirm」ボタンをクリックします。
  3. Channel ID」に入力した「laundryreminder」だけ控えておきます。表示されているAppkeyやAppSecretは使用しないので無視します。
  4. 再びナビゲーションメニューから「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アカウントでログインしてマイぺージを開きます。

f:id:IntellectualCuriosity:20200515202122p:plain

アクセストークンの発行(開発者向け)から「トークンを発行する」をクリックします。

f:id:IntellectualCuriosity:20200515202838p:plain

発行されたトークンを控えておきます。

 

ラズパイでの作業

dataplicityのインストール

dataplicity.comにアクセスしてアカウントを作ります。

こんな表示があるのでメールアドレスを入力して先を続けてください。

f:id:IntellectualCuriosity:20200515010844p:plain

サインインするとこんな画面が表示されます。

表示されているスクリプトをラズパイで実行するとdataplicityがインストールされます。

f:id:IntellectualCuriosity:20200515012011p:plain

インストール後に前のページをリロードすると、デバイスにラズパイが表示されます。

f:id:IntellectualCuriosity:20200515014153p:plain

ラズパイをクリックすると、右側に緑色のワームホールスイッチがあるのでオンにすると、その下に表示されているURLでインターネットからラズパイにアクセスできるようになります。

f:id:IntellectualCuriosity:20200515014732p:plain

重要なのが赤枠で囲った内容で、トンネリングでアクセスできるのは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を行います。

  1. スマートプラグの電源ボタンを長押し(5秒ぐらい)してリンキングモードにします。このとき、Smart Lifeアプリを立ち上げてデバイスリストからリンキングモードにしたスマートプラグが削除されるのを確認します。
    ※スマートプラグが複数あるときはひとつづつ5~7を繰り返します。
  2. ターミナルで以下の引数を指定してtuya-cliコマンドを実行します。
    --apikey : クラウドの設定で取得した「Access ID/Client ID」
    --api-secret : クラウドで取得した「Access Secret/Client Secret」
    --schema : クラウドで設定した「Channel ID」(laundryreminder)
    --ssid : スマートプラグに設定するWiFiSSID
    --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
  3. 成功すると次のように表示されます。
    ✔ Device(s) registered!
    [
      {
        id: 'xxxxxxxxxx',
        ip: 'xxx.xxx.xxx.xxx',
        localKey: 'xxxxxxxx',
        name: 'WP6  Mini Smart Plug'
      }
    ]
    idとlocalkeyに表示された値を控えておきます。

 

ファイル構成

ファイル構成はこんな感じになっています。

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ピクセルで作ってください。

f:id:IntellectualCuriosity:20200516153349p:plain f:id:IntellectualCuriosity:20200516153438p:plain

 

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とかなら必要ないかも知れません。

 

おわり