当記事は、スマートな加湿ストリーマ空気清浄機を更にスマート化からスクリプト類の転載含め、編集したものです。
任意のスマート家電用にブラウザによる操作パネルと自作Julius/Open JTalkスマートスピーカー機能からも操作できるスクリプトを自作してみた話。
この操作パネルなら、パソコンでもスマホやタブレットでも使えるので、なんならスマホ専用アプリも要りませんし、Wi-Fiのみで済み、クラウドを経由する必要もなく、LAN内で完結させることができます。
送信コマンドを確認するにあたり、専用アプリもいりますが。
また、自作pythonスクリプトは、自身はラズパイスマートスピーカー専用機や2台のパソコンに入れた同機能から音声操作できるようにと作りましたが、引数を渡すだけでスマート家電を操作できます。
これも連携しているAIスマートスピーカーに、そもそもスマートピーカーに限らず、他用途でも使えます。
「任意のスマート家電用に」と書きましたが、ここで紹介した空気清浄機用のパネルやスクリプトをそのまま使えるという意味ではなく、この手順を踏むことで、全てとは言わないまでも、たいていのスマート家電において汎用的に操作コマンドやデータ取得コマンドを得ることができるという意味です。
スマート家電と言えばというほど、スマホ専用アプリか、あって市販AIスマートスピーカーから操作するものが大半。
それはそれとしてもパソコンからも操作パネルで、自作のJulius/Open JTalk製スマートスピーカーでも音声で操作したいよねってことで。
尚、ここで掲載した自作ブラウザ操作パネルや自作スクリプト等々は我が家初の最初からスマート家電であるダイキン加湿ストリーマ空気清浄機で操作コード含め、実際に使っているもの、そのままです。
このスマート家電がもつセンサー機能による温度・湿度、センサー値(PM2.5/ホコリ/ニオイ)は、自作ブラウザ操作パネル上でも画面をリロードすることなく、当該部分だけをリアルタイム表示(というか例では、ほぼほぼ1分ごとに更新)できるようにしてあります。
ちなみに、この空気清浄機の専用アプリ、とあるバージョンから会員登録、ログイン、クラウド経由が必須となったとのことでしたが、それ以前の旧バージョンのアプリを使い続けることで、これらを回避し、LAN内で登録から操作まで全て完結、外からはVPNで代替しています。
ブラウザパネルができてしまった後は、スマホでもそれを使えば、以後、アプリのバージョンも関係ありませんから、今回の場合だと旧バージョンのアプリはアンインストールしても良いんですけどね。
自動バージョンアップを回避するためのGoogle Play Storeの自動更新を停止しなくても済みますし。
Webサーバが必要となり、これ(パソコンに建てるならパソコン)が起動中のみ利用できます。
IPを固定でき、かつ、オリジンポリシーを回避できたり、スマート家電側のCORSステータスを変更できる場合は、サーバは不要ですが...。
たいていはいけると思いますが、全てのスマート家電、IoT家電が、ここで述べる方法で操作可能となる保証はありません。
検証環境については、クライアントマシンは、ARM64/ARMv8 Raspberry Pi 400/Raspberry Pi OS 64bit( + 512GB SSD + 1TB HDD + Wi-Fiマウス + 19インチ液晶モニタ)、基本、Firefox、たまにChromiumで検証。
ちなみにX11|Xorgではなく、Wayland版のデスクトップ上でWaydroidを使うに当たり、必要なカーネルオプションを有効にしたオリジナルカーネルを使っています。
サーバは、ARM64/armv7l Raspberry Pi 3B+、OSはDebian Linux系Raspberry Pi OS 32bitです。
何度か入れ替えたOS、64bitにしていたとばかり思っていたのですが、32ビットでした。
尚、このサーバは、常時ではなく、必要都度起動させて運用しています。
よってサーバを起動していない場合に備え、すっかりメインとなったクライアントとしたRaspberry Pi 400パソコンにも同操作パネルを入れ、自動起動させて利用できるようにしました。
滅多に起動することもなくなったサブとなって久しいdynabookには、入れていませんが、起動頻度が上がれば、入れておいても良いかなとは思っています。
まず、スマホにスマート家電専用のアプリをインストール、操作できるようにしておきます。
更にIOT家電をAPIハックして、Siriから簡単に操作する方法や無料プロキシツール「mitmproxy」を使ってみよう - セットアップ方法とセキュリティエンジニアおすすめの設定などを参照の上、パソコンなどにプロキシアプリをインストール・起動させ、パソコンとスマホを同一SSIDのWi-Fiに接続します。
続いて前者のリンク先の通り、スマホ側で当該Wi-FiのSSIDに「手動」で「プロキシを追加」、パソコンのプロキシアプリで設定したIPアドレスとポートを設定。
概要ながら、ネット接続できない場合などポートと完了後もmitproxy要スクロールな点に注意。
この状態でスマホ専用アプリから操作することでパソコンのプロキシアプリで各種操作コマンドやセンサー値取得コマンド等を一通り確認し、控えておきます。
控えた後は、もう必要ないのでスマホで手動に設定したプロキシ設定は「なし」に設定し直し、パソコン側のプロキシアプリも停止します。
パソコンなどにWebサーバを建てブラウザをクライアントとして任意のサーバスクリプトとクライアントスクリプトでリアルタイムにデータを送受信できる状態にします。
任意のサーバスクリプト上でスマート家電のIPアドレスを取得、先ほど控えたスマート家電の各種操作用コマンドパスと併せて、たいていのスマート家電にはあるであろう現在の運転情報、必要ならセンサー情報等のパスとパラメータを取得、クライアントスクリプト側に送信します。
このデータを元に画面表示するなどクライアントスクリプト側で善きに計らいます。
画面表示に時間がかかるようならCSSなどでフェードインさせるなどして対処します。
自身はESP32ボードで非スマート家電を続々とスマート化していますが、Wi-Fi経由で操作できるもの(今回の場合、スマート家電上に)は、Webサーバがあり、そのIPアドレスに対してGETやPOSTなどの方法でデータを送受信することで操作するものが一般的かと思います。
そんな中、スマート家電においては、次のようにIPアドレスの固定や特定、CORSステータスの変更ができないという2つの点でスマート家電が備えているであろうWebサーバとは別にWebサーバが必要と判断しました。
1つめのIPアドレス特定については、多くのスマート家電においては、どうもIPアドレスを固定する方法がないものの方が多い模様です。
iPhoneのように容易にmDNSを使えれば良いのですが、Androidでは素直に使えません、仮にiPhoneを使うとかAndroidを使わないにしても、もう1つの課題があります。
他方、ルーターの機能で「特定の」IPアドレスを固定できればよいのですが、今回、自身の環境のように、これができないケースもあります(し、できても、もう1つ課題が...)。
そうなると仮にIPが変更になっても対応できるようにする...そうするには、都度、IPアドレスを調べる必要が出てきます。
IPアドレスを調べるにも対象が複数あるとどれがそうなのかを特定する必要が生じます。
よってMACアドレスなら世界で一意のはずなので、これを元にIPを特定することにしました。
しかし、ブラウザ用スクリプトからは、Wi-Fi環境内にある全てのMACアドレスからIPアドレスを調べるということはできません。
2つめのCORSステータスの変更ができない件。
自身は、ESP32ボードなどで家電や照明、カーテンなどをスマート化しており、この壁にぶつかったことはありませんでした。
が、このケースを例にざっくり言えば、ESP32側で受信する相手を選別・特定できる仕組みがオリジンポリシーと言われるものでAjaxの登場により、これを許容できるように作られた仕組みがCORS/Cross-Origin Resource Sharing(クロスオリジンリソース共有/オリジン間リソース共有)とのこと。
オリジンポリシーにより悪意ある送信者が受信者から情報を漏洩させるような悪事を防ぐことができる素晴らしい仕組みとのこと。
当然のごとく?どうやらスマート家電でも専用アプリ以外からは拒否している模様。
CORSのステータスを誰もが勝手に変更できては、セキュリティの意味をなしませんから、他からのアクセスを許容するようには変更できないわけです。
これは、往々にして情報漏えい防止策の側面が強く、データ送信は拒否されず、データの受信に対して選別されるブラウザにおける対策です。
つまり、ブラウザからスマート家電への操作コマンドは送信できますが、運転情報やセンサー情報の取得などは、スマート家電側によって許可されていないとできないことになります。
とは言え、これは、ブラウザからはという話であり、情報取得については、ブラウザスクリプト等ではなく、サーバスクリプトなどOS上で利用するスクリプトやアプリなら制約なく、受信できます。
というわけでWebサーバ側のスクリプトで「IPを特定」、スマート家電から「情報を取得」し、それをクライアントであるブラウザ側のスクリプトに送信すれば、前述の2つの課題をクリアできるというわけです。
総数にもよるとは言え、IPアドレスを割り出すには多少の差はあれ、時間はかかりますが。
逆に言うと、せめてIPアドレスの固定さえできれば、または、mDNSでアクセスできれば、時間はかからないのですが。
今回、サーバサイドスクリプトには、Node.js、クライアントサイドスクリプトには、JavaScriptを使うことにしました。
というか、Node.jsはスクリプト言語やプログラミング言語ではなく、JavaScriptと極めて親和性が高いOS上でJavaScript/TypeScript/ECMAScript等が動く実行環境とのこと。
自身は、Node.jsを使うのは、ほぼ初めて、JavaScriptは使ったことはありますが、変遷が激しく、例えば、オブジェクト指向になる前後の明確な違いとか、イベントとか、コールバック関数とか、正直あやふやな感じ。
また、HTTPと併せてリアルタイム(に限りなく近い)通信には、いくつかあるようですが、WebSocket(Socket.io)を使うことにしました。
WebsocketはESP32ボードでJavaScriptと併せて使ったことがありますが、Node.jsとはだいぶ勝手が違い、JavaScriptの他の部分が、あやふやだったこともあり、更に、ややこしさを感じました。
それでも当初、PythonフレームワークのFlaskで始めてみたら、WebsocketやJavaScriptとの絡みが分かりづらく、もしやNode.jsなら...と乗り換えた次第。
不慣れにつき、お作法通り、定石通りできているのか自信はありませんが、期待通り、動いているので良いかなと...。
それにしても、できてみれば、そこそこ苦戦した割には、なんともステップ数の少ないこと...IT/IoTすごし。
まずは、Node.jsのインストール。
自身のようにホストがLinuxならリポジトリから、または、本家Node.jsダウンロードページからダウンロードしてインストールします。
続いてNode.js内のモジュール管理ができるパッケージマネージャnpmのインストール。
とは言え、npmはNode.jsをインストールした際に一緒にインストールされることが多いでしょう。
そうでなければ、リポジトリなどからインストールします。
使い方は、今回に関しては、npm install(npm uninstall)、npm init、npm list、npm search程度で十分ですが、端末からのバージョン確認は、node、npmにそれぞれ、--version|-vをつければOK、npmの最新版へのアップグレードはnpm install -g npm@latestなどとします。
プロジェクトごとのルートとすべく、任意のディレクトリを用意します。
次にnpm initとするようで対話形式で応答した結果としてpackages.jsonというファイルができます。
このpackage.json、なくても良さげと思いきや、作成後、npm install --save(実際は--saveはなくても)した時に自動的に[dependencies](依存関係)としてインストールしたものが追記されたりします。
依存関係だけならnpm listでも参照できますが、このファイルとの関連は未調査。
ちなみに新たなプロジェクトを作る場合、既存のプロジェクトディレクトリをコピーするのはやめておいた方が賢明なようです。
例えば、package.jsonやnpm listが反映されなかったりしますので。
同じ環境を作る場合は...と思いきや、あなたがnpm installをしてはいけない時によれば、その場合もちょっとした注意が必要なようです。
ソースを書いてからというケースもありますが、必要となるモジュールをnpmでインストールしておきます。
と思ったら、socket.ioモジュールとnode-cronモジュール、pathモジュール(pathモジュールは入ってるかも)以外は、Node.jsをインストールした時点で同梱されていました。
package.jsonやnpm list結果にもバージョン付きのインストールしたパッケージが反映されているはずです。
今回は、141223_nodejs_socketio/SocketIOTest/server.jsを参考に作成したものを仮にserver.jsとします。
const server = http.createServer(requestListener);のデフォルトがそうなのか、IPアドレスはLAN内のアドレス範囲でもlocalhost|127.0.0.1でもOKでした。
また、server.listen((process.env.PORT || 5000), function () {});でポート番号を指定しています。
よってlocalhost:5000や192.168.1.1:5000などとすれば、server.jsのあるディレクトリにあるindex.htmlが表示されます。
クライアント側のJavaScriptは、同ディレクトリの下のjs/ディレクトリ、同様にCSSは、css/ディレクトリ以下に配置しています。
尤もこれは、pathモジュールやfsモジュール、requestListener関数と、そこから呼ばれるreadFileHandler関数のおかげで、index.htmlで指定する時に、当該プロジェクト内であることを前提にパスを示しさえすれば、どこでも良いようです。
node_module/ディレクトリ、package-lock.jsonは自動的に作成されます。
Webサーバが必要となった理由であるスマート家電のIPアドレスの取得と運転情報の取得には、外部コマンド(shell)を実行できるexecコマンドを使いました。
前者については、nodejsでやれる方法があるのか探す以前にその方が手っ取り早いと思ってarp-scanとawk、後者については、サーバサイドでも影響を受けるのか、requestだと取得できなかったのでcurl。
arp-scanにはsudo(管理者権限)がいるので当該ユーザーがパスワードなしでsudoを実行できるようにするなど善きに計らっておく必要があります。
ネットワークアドレスとアドレス範囲、MACアドレスを変更、クライアント側も後述のようにすれば、MCK70Y他、後述の対象機器を持っていれば、そのまま操作できるはずです。
初期表示や操作時に使う情報として操作パラメータ、センサー値、スマート家電のIPアドレスを、クライアントからのコールバックありとしてsocket.emit()でクライアント側でも同名を元にデータを取得することになる任意のacinfoという名前空間でブラウザ側スクリプトにJson形式({ key:val })で送信しています。
また、この例では、1分ごとにセンサー値を更新すべく、node-cronを使って、サーバからクライアントに一方的に送信、コールバックがないものとしてio.emit()でクライアント側でも同名を元にデータを取得することになる任意のsensor_infoという名前空間でブラウザ側スクリプトにJson形式({ key:val })で送信しています。
尚、ここでは、クライアントはブラウザを指します。
node.jsのメインアプリを拡張子.jsで作成、実行は、node *.jsとします。
クライアント側として、まずは、141223_nodejs_socketio/SocketIOTest/js/main.jsを参考にJavaScript。
初期値と操作時にサーバ側からsocket.emit()で指定した任意のacinfoという名前空間で受信。
Json形式なのでクライアント側関数の引数として任意に名付けたinfo['']の恰好で取得。
['']にはサーバ側で指定したkey/キー名(params,sensors,url)を指定しています。
今回、サーバ側から運転情報のパラメータ、スマート家電によるセンサー値とスマート家電のIPアドレスを受信。
これを元にブラウザ上に表示する操作パネルの状態を設定しつつ、操作しています。
尚、今回サンプルの空気清浄機については、ON/OFF以外、操作パラメータは、常に4つ渡す必要あり。
続いてHTML。
単なる操作パネル。
サーバーサイドのserver.jsがあるディレクトリをルートとするということなのか、もしくは、相対パスではなく絶対パスとするということなのか、index.htmlでパス指定する際は、[/js/*.js]、[/css/*.css]など先頭に[/]が必要です。
各種センサー値を表示することにし、画面をリロードせずに、これら値、一部分のみを定期的に更新すべく、body内でJQueryを使っています。
サーバ側スクリプトから定期的に一方通行で送られてくるセンサー値のみを受信すべく、io.emit()で指定した任意のsensor_infoという名前空間で受信。
Json形式なのでクライアント側関数の引数として任意に名付けたinfo['']の恰好で取得。
['']にはサーバ側で指定したkey/キー名(sensors)を指定しています。
尚、モバイル対応を考えるとON/OFF以外は、CSSでというより、そもそもラジオボタンじゃなく、使用する要素について再考の余地ありかなと。
そしてCSS。
ON/OFFは、ON/OFFスイッチをCSSのみで実装の「グラデーションのスイッチボタン」をまんま頂戴しました。
前述のようにIPアドレスの取得に一定の時間がかかります。
そこでHTML表示自体を遅延させることにしました。
ついては、namnium1125氏のコードと徐々にフェードイン、フェードアウトさせるアニメーションサンプルを参照。
その上で即表示される「しばらくお待ちください...」メッセージがフェードアウトすると同時に最新の運転状態が反映された操作パネルがフェードインするように設定しました。
最初にページを開いた時、温度や湿度、他センサー値が表示されていない、運転中なのに該当するモードや風量が選択されていないといった場合は、[F5]などで画面更新を。
そうなる原因は基本2つ、1つはWebサーバ起動直後、IPアドレス取得ほか、まだ準備ができてない段階で操作パネルを開いた、2つめは、そもそもIPアドレス取得にかける時間が微妙なためと思われます。
前者は、開発時以外(後述の自動起動後)は起こり得ない、後者であれば、Promise関数のTimeout時間をもう少し長めにとることで対処できます(これに伴い、延長する時間によってはCSSでメッセージのフェードアウト・画面のフェードイン時間を延長する必要もありますが)。
また、クライアント側JavaScriptにおいて2度手間なロジックを書いている部分がある気はしているものの、正常に機能していることもあり、そのままにしてあります(が、気が向いたら修正します)。
あと、これは何れも気のせいかもしれませんが...
何れかのみなら問題ないものの、スマホとパソコンから交互に操作した時、ボタンが機能しないとか、少しおかしな挙動を示すことがあるような?
サンプルとした空気清浄機については、会員登録すれば、複数のスマホから空気清浄機を操作できるとあるので、今回、会員登録やログイン、クラウドも経由せずに使っているとは言え、もしそうなら、自身の作りの問題かと。
尤も自作操作パネルでは、スマホとPC両方で同時に同操作パネルを開いている場合、一方での操作時に他方のパネルを自動更新するようには作っていないので、そのせいかもしれませんが。
メーカーが複数のスマホでも操作できるといっているケースも同時に開いた操作パネル上の話ではなく、別々にならということであれば同じですが、公式アプリでこれを試してみたことがないので実際のところは、わかりません。
もし、そうなったら、メインメニューから、もしくは、改めてブラウザ上でアドレスとポートを指定すれば、正常動作するかと。
再現するものなのか含め、しばらく使いながら正確な状況を把握できればと思ってはいますが。
尚、サンプルとした空気清浄機において加湿の「自動」は、「おまかせ」モードと「のど・はだ」モード専用で手動設定不可、これらコースを選択時、自動的に「自動」がチェックされ、これらコースからそれ以外のコースや風量を選択した場合のみ、加湿が自動的に「OFF」となり、その後は、加湿のモードを任意に設定することができるという仕様になっています。
加湿の自動OFFについては、なぜか、この状況においてのみ、「おまかせ」コースや「のど・はだ」コース選択前の値を取得できなかったからですが、公式アプリでは、これらコースを選択する前の加湿モードに移行できるので、自身が何か勘違いしているだけのようですが。
ちなみに同一の名前空間でサーバとクライアント間においてデータ送受信する際、サーバ側のkey-valueのkey名とクライアント側で受け取る変数名を同じにしてしまうと無駄にハマります。
2台それぞれのPCに、メインのパソコンに...とも思いましたが、これらの他、都度起動の既存のラズパイサーバにもWebサーバを置くことに。
ラズパイサーバを起動している場合は、自作スマートホームブラウザ操作パネルから、ラズパイサーバを起動してない場合は、デスクトップにショートカットを置いてfirefox-esrにlocalhost:*を渡す恰好で。
そのラズパイサーバにnode.jsプロジェクトを移行、このラズパイサーバが起動した時にNode.js Webサーバを自動起動させます。
ラズパイサーバ起動時に自動起動させるべく、systemdのサービスファイルを作成、ファイル名はもちろん任意。
実行ファイルをホームディレクトリに置いた例、USERやパス、ファイル名は適宜変更。
動いているし、たぶん、こんな感じで良いんじゃないかと。
/bin/sh -cとしたからか、shellスクリプトじゃないとうまくいかなかったのでラッパスクリプト。
サーバ側スクリプトをバックグラウンド起動。
隠しファイルとして(.付けて)みた例、ファイル名は任意ですが、当然、systemdファイル内の[ExecStart]で指定の名称と合わせる必要あり。
ラッパスクリプトに実行権限を。
でサービス開始と次回起動時、自動起動設定。
一方、専用機としてのラズパイや2台のパソコンに入れた自作Julius/Open JTalkスマートスピーカー機能は、これらがコンピュータなので、こんなまどろっこしいことをしなくともWebサーバも要らず、pythonスクリプトだけで良いので簡単です。
このスクリプトを自作スマートスピーカーの仕組みに取り込む必要はありますが。
と言っても既存の自作スマートスピーカーのスクリプトにおいて操作する音声に符合する条件に、それぞれ、このスクリプトと引数を指定する程度。
これは、PythonでMAC AddressからIP Addressを調べるスクリプトとakiraseto / daikinCleanerのスクリプトの合作で、ほんのちょっとだけ手を加えたものです。
操作パネルでは取得したget_sensorの他、basic_infoなどこのスクリプト版では使用していない行もありますが、音声操作でスマート家電をONした際などに、これらの値を読み上げるのもありかもしれませんね。
ipaddr = scan()行のMACアドレスとネットワークアドレス+アドレス範囲だけ環境に合わせて変えれば、そのまま使えるはずです。
ファイル名にif-elif-else文のargにあたる各フレーズを1つ引数として渡すだけ。
端末上から実行する場合、結果、IPアドレス、引数、通ればret=OKの戻り値を表示します。
同時に前述の操作パネルと同様に実際のコマンドパスを使っているので後述の対象となるダイキン加湿ストリーマ空気清浄機があってWi-FiとpythonスクリプトでMACアドレスとネットワークアドレスを指定し、Pythonを実行できる環境が整っていれば、実際に作動します。
ただ、arpするのに管理者権限(sudo)がいる...、そりゃそっか。
結果、スマホ・タブレットに限らず、パソコンからでもブラウザ版操作パネルから、ラズパイやパソコンに入れたJulius/Open JTalkスマートスピーカー機能からでも音声操作できるようになりました。
ここで掲載しているスクリプト類は、ダイキン加湿ストリーマ空気清浄機MCK70Yをサンプルとしており、操作コマンドも実際のもの、参照リンク先等も併せて考えると、少なくともリンク先に書いた機種(ACK|TCK|MCK 70 W|X|Y|Z)で実際に操作可能と思われます。