LoginSignup
43
49

More than 5 years have passed since last update.

Raspberry PiとFirebaseでブラウザからエアコン操作

Posted at

いきさつ

今住んでるマンションのエアコンはオフタイマーとオンタイマーを同時に設定できなく、夜オフタイマーをつけると、朝は寒い中起きてこなくちゃいけない。去年買ったRaspberry Piが使われずに転がってたので、これを使って問題を解決してみることにした。

Raspberry Piでリモコン赤外線を送受信する方法は、

  • 赤外線LEDと受信モジュールを使って自前で回路を実装してGPIOで通信する
  • USB接続の赤外線リモコンキットを買ってくる

の2通りが考えられるけど、半田ごてが見当たらなかったので、キットを買ってくることにした。ネットで調べるとUSB接続 赤外線リモコンキット(株式会社ビット・トレード・ワン)irMagician(大宮技研 合同会社)辺りが、Raspberry Piでの実績がすでにあるらしい。irMagicianの方がメモリ容量も大きいので長い信号に対応してそうな上、サイズも小さいので、irMagicianを使うことにした。

インターネットからRaspberry Piに直接アクセスできるようにするにはルーターのポートを開けたりする必要があり、面倒なので、今回はFirebase Realtime Databaseを使って通信することにした。また、自分以外の人にエアコンを操作されたくないので、Firebase Authenticationを使ってGoogleアカウントで認証するようにした。

aircon.png

購入

品物 値段(円)
irMagician 3,980
Raspberry Pi 3 5,800
Raspberry Pi 3 ケース 1,280
ACアダプタ 5V2.5A 1,100
USBケーブル 1.5m 120
マイクロSDカード16GB 900

irMagicianはITプラザで完成品を購入。(はんだ付けに自信があれば1,980円のキットのほうがお得。)それ以外は秋月電子で購入。Raspberry Pi 2 Model Bをすでに持ってるけど、Raspberry Pi 3の方がWi-FiとBluetooth(今回は使わない)を内蔵してるし、せっかくなので、と思い衝動買い。

これ以外に、マイクロSD->SDカードアダプタ、ACアダプタからの給電用USBケーブル、USBキーボード、HDMIケーブルとディスプレー(テレビで可)が別途必要。

セットアップ

Macで起動用SDカードの作成

公式ページから最新のRASPBIAN JESSIE LITEをダウンロード。(12月4日現在2016-11-25版、2016-11-25-raspbian-jessie-lite.zipが最新)

念のためSHA-1 ダイジェストがダウンロードページに書いてあるSHA-1と一致してるのを確認して、解凍。

$ openssl sha1 2016-11-25-raspbian-jessie-lite.zip
SHA1(2016-11-25-raspbian-jessie-lite.zip)= 6741a30d674d39246302a791f1b7b2b0c50ef9b7
$ unzip 2016-11-25-raspbian-jessie-lite.zip
Archive:  2016-11-25-raspbian-jessie-lite.zip
  inflating: 2016-11-25-raspbian-jessie-lite.img  

SDカードをMacに挿す前と後でdiskutil listを比較して、増えた/dev/diskNを確認。(私の環境では/dev/disk2。) diskutil unmountDiskでディスクをアンマウントし、ddコマンドでディスクイメージをSDカードに書き込む。ddコマンドはかなり時間がかかる。Ctrl+Tで何バイト書き込まれたか確認できる。

$ diskutil unmountDisk /dev/disk2 
Unmount of all volumes on disk2 was successful
$ sudo dd if=2016-11-25-raspbian-jessie-lite.img of=/dev/disk2 bs=1m

Raspberry Piの初期設定

Raspberry Piをケースに入れて、SDカードを挿して、HDMIケーブルでテレビに繋いで、USBキーボードを挿して、USBアダプタから電源を供給すると初回起動が始まるので、ログイン画面まできたら、ID: pi、Password: raspberryでログインし、sudo raspi-configで初期設定。

  • 2 Change User Password でパスワードの変更
  • 4 Internationalisation OptionsI2 Change TimezoneAsiaTokyo
  • 4 Internationalisation OptionsI3 Change Keyboard LayoutGeneric 105-key (Intel) PCOtherJapaneseJapaneseThe default for the keyboard layoutNo compose key
  • 4 Internationalisation OptionsI4 Change Wi-fi CountryJP Japan
  • 7 Advanced OptionsA4 SSHYes

sudo shutdown -r nowで再起動し、sudo vi /etc/wpa_supplicant/wpa_supplicant.confでWi-fiの設定。

wpa_supplicant.conf
country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="Wi-fiのSSID"
    psk="Wi-fiのパスワード"
    key_mgmt=WPA-PSK
}

そして、sudo shutdown -r nowで再起動。画面にMy IP address is 192.168.0.10 ...とIPアドレスが出てくるのでメモってMacからssh pi@192.168.0.10でログインできることを確認。

最後に、sudo apt-get updatesudo apt-get dist-upgradeでパッケージの更新をして初期設定は終了。

irMagicianの動作確認

Raspberry PiとirMagicianをUSBで接続し、dmesgコマンドでttyACM0として認識されたのを確認する。

dmesg
$ dmesg
[    0.000000] Booting Linux on physical CPU 0x0
...
[  275.569696] usb 1-1.4: new full-speed USB device number 4 using dwc_otg
[  275.692440] usb 1-1.4: New USB device found, idVendor=04d8, idProduct=000a
[  275.692461] usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[  275.692475] usb 1-1.4: Product: The Ultimate irController - irMagician
[  275.692487] usb 1-1.4: Manufacturer: Microchip Technology Inc.
[  275.692500] usb 1-1.4: SerialNumber: 0123
[  275.714912] cdc_acm 1-1.4:1.0: ttyACM0: USB ACM device
[  275.715694] usbcore: registered new interface driver cdc_acm
[  275.715706] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters

動作確認のために使うscreenコマンドをsudo apt-get install screenでインストールし、screen /dev/ttyACM0 9600で、irMagicianと通信開始。vキーを押して、Enterキーを押すと、画面に

irMagician 1.0.1
OK

と表示されていたらirMagicianと通信は成功。

次は信号の学習の動作確認。cキーを押して、Enterキーを押すと、画面に...と表示されるので、テレビリモコンのボタンを押してirMagicianのセンサに赤外線を当てる。... 整数という表示になれば成功。... Time Out !と表示されたら失敗。

最後に、信号の再生の動作確認。irMagicianをテレビに向けて、pキーを押して、Enterキーを押すテレビの電源が切れるor入れば成功。Ctrl+a kでscreenを終了。

irMagicianと通信するPythonプログラム

開発元の大宮技研のサイトで、irMagicianと通信するPythonプログラム例が公開されている。このプログラムはpyserialというパッケージを使っているので、まず、pipをインストールしてから、pyserialをインストールする。

$ sudo apt-get install python-pip
$ sudo pip install pyserial

cap.pyplay.pysaveIrMagician.pyplayIrMagicianLocal.pyをダウンロード。

$ mkdir ~/irm
$ cd ~/irm
$ wget http://www.omiya-giken.com/irMagician/python/linux/cap.py
$ wget http://www.omiya-giken.com/irMagician/python/linux/play.py
$ wget http://www.omiya-giken.com/irMagician/python/linux/saveIrMagician.py
$ wget http://www.omiya-giken.com/irMagician/python/linux/playIrMagicianLocal.py

cap.pyで赤外線信号を学習し、play.pyで再生が成功していたら、saveIrMagician.py filenameで信号情報をファイルに保存、playIrMagicianLocal.py filenameでファイルから信号情報を読み取って送信できる。

今回は適当な温度設定でエアコンをオンにする信号情報on.jsonと、オフにする信号情報off.jsonを作成しておく。

Firebaseの登録

Firebase Consoleにログインし、「新規プロジェクトを作成」をクリック。
適当なプロジェクト名を入力して、「プロジェクトを作成」をクリック。
ギアのアイコンをクリックし、「プロジェクトの設定」をクリックし、「プロジェクト ID」をメモっておく。

左のメニューの「Authentication」をクリックし、「ログイン方法」、「Google」を選択し、「有効にする」にチェックを入れ、保存をクリック。

「ウェブ設定」をクリックし、configの中身をメモっておく。

  // Initialize Firebase
  var config = {
    apiKey: "XXXXXXXXXXXXXXXX-XXXXXXXXXXXX",
    authDomain: "プロジェクト ID.firebaseapp.com",
    databaseURL: "https://プロジェクト ID.firebaseio.com",
    storageBucket: "プロジェクト ID.appspot.com",
    messagingSenderId: "0123456789"
  };
  firebase.initializeApp(config);

Firebase コマンドラインツール

手元のパソコン/MacにFirebase コマンドラインツールをインストールしてコマンドでFirebaseをいじれるようにする。(手元のパソコン/MacにもNodeJSをインストールしておく必要がある。)

$ npm install -g firebase-tools

Firebaseの設定するためのディレクトリを作成し、firebaseコマンドで設定する。

$ mkdir ~/firebase_prj
$ cd ~/firebase_prj
$ firebase login
? Allow Firebase to collect anonymous CLI usage information? (Y/n) # これはお好みで選択

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=......
# 表示されたURLをブラウザでアクセスし、先程Firebaseに登録したGoogleアカウントでログインする。
Waiting for authentication...

✔  Success! Logged in as あなたのGooogleアカウントアドレス
$ firebase init
# 中略
? What Firebase CLI features do you want to setup for this folder? (Press <space> to select)
❯◉ Database: Deploy Firebase Realtime Database Rules
 ◉ Hosting: Configure and deploy Firebase Hosting sites
# 両方選択した状態で、Enter
=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? What Firebase project do you want to associate as default? 
# 先ほど作成したプロジェクトを選択し、Enter
=== Database Setup

Firebase Realtime Database Rules allow you to define how your data should be
structured and when your data can be read from and written to.

? What file should be used for Database Rules? (database.rules.json) 
# Enter
=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? (public) 
# Enter
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) 
# Enter
✔  Wrote public/404.html
✔  Wrote public/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!
$ ls
database.rules.json firebase.json       public

database.rules.jsonを下記のように変更。

database.rules.json
{
  "rules": {
    ".read": true,
    ".write": "auth.provider == 'google' && auth.token.email == '自分のGoogleアカウントアドレス@gmail.com'"
  }
}

public/index.htmlを下記のように変更

index.html
<script src="https://www.gstatic.com/firebasejs/3.6.1/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
  apiKey: "XXXXXXXXXXXXXXXX-XXXXXXXXXXXX",
  authDomain: "プロジェクト ID.firebaseapp.com",
  databaseURL: "https://プロジェクト ID.firebaseio.com",
  storageBucket: "プロジェクト ID.appspot.com",
  messagingSenderId: "0000000000"
};
firebase.initializeApp(config);

function loginOrLogout() {
  if (firebase.auth().currentUser) {
    // Logout
    firebase.auth().signOut().then(_ => console.log('Logout: OK '),
                                   _ => console.log('Logout: Error'));
  } else {
    // Login
    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope('https://www.googleapis.com/auth/plus.login');
    firebase.auth().signInWithRedirect(provider);
  }
}

function sendEnabled(value) {
  firebase.database().ref('aircon')
      .set({value: value, timestamp: firebase.database.ServerValue.TIMESTAMP})
      .then(_ => console.log('sendEnabled: OK'),
            _ => console.log('sendEnabled: Error'));
}

function sendTimer() {
  firebase.database().ref('timer')
      .set({
            on: {
              enabled: document.getElementById('onCheck').checked,
              time: document.getElementById('onTime').value
            },
            off: {
              enabled: document.getElementById('offCheck').checked,
              time: document.getElementById('offTime').value
            },
            timestamp: firebase.database.ServerValue.TIMESTAMP
          })
      .then(_ => console.log('sendTimer: OK'),
            _ => console.log('sendTimer: Error'));
}

function initialize() {
  firebase.auth().onAuthStateChanged(user => {
    document.getElementById('loginOrLogout').value = user ? 'Logout' : 'Login';
  });
  firebase.database().ref('/')
    .on('value', snapshot => {
      var timer = snapshot.val().timer;
      if (!timer)
        return;
      if (timer.on) {
        if (timer.on.enabled)
          document.getElementById('onCheck').checked = timer.on.enabled;
        if (timer.on.time)
          document.getElementById('onTime').value = timer.on.time;
      }
      if (timer.off) {
        if (timer.off.enabled)
          document.getElementById('offCheck').checked = timer.off.enabled;
        if (timer.off.time)
          document.getElementById('offTime').value = timer.off.time;
      }
    });
}
window.addEventListener('load', initialize);
</script>

<input type="button" id='loginOrLogout' onclick="loginOrLogout()" value="">
<div>
<input type="button" onclick="sendEnabled(true)" value="On">
<input type="button" onclick="sendEnabled(false)" value="Off">
<input type="button" onclick="sendTimer()" value="Set Timer">
</div>
<div>
<label><input type="checkbox" id="onCheck">On: </label>
<input type="time" id="onTime">
</div>
<div>
<label><input type="checkbox" id="offCheck">Off: </label>
<input type="time" id="offTime">
</div>


そして、 firebase deployでデプロイ。

$ firebase deploy

これで、 https://プロジェクトID.firebaseapp.com で遠隔操作画面にアクセスできる。

Raspberry PiにNodeJSをインストール

$ sudo apt-get install git
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
$ nvm ls-remote      # Latest LTSを確認
$ nvm install v6.9.1 # 上で確認したLatest LTSを選ぶ

Raspberry Piからfirebaseに繋いでリモコンを操作するプログラム

$ mkdir ~/prj
$ cd ~/prj
$ npm init
$ npm install --save firebase

下記のようなaircon.jsをprjディレクトリに作成する。

aircon.js
var firebase = require("firebase");
var config = { databaseURL: "https://プロジェクトID.firebaseio.com" };
firebase.initializeApp(config);

var execSync = require('child_process').execSync;

var AIRCON_ON_COMMAND =
    'python /home/pi/irm/playIrMagicianLocal.py /home/pi/irm/on.json';
var AIRCON_OFF_COMMAND =
    'python /home/pi/irm/playIrMagicianLocal.py /home/pi/irm/off.json';


firebase.database().ref('/aircon').on('value', function(snapshot) {
  console.log('/aircon: ' + JSON.stringify(snapshot.val()));
  if (snapshot.val().value) {
    console.log('aircon on');
    console.log(execSync(AIRCON_ON_COMMAND).toString());
  } else {
    console.log('aircon off');
    console.log(execSync(AIRCON_OFF_COMMAND).toString());
  }
});

var onTimerInfo = {enabled: false};
var offTimerInfo = {enabled: false};

function fireOnTimer() {
  console.log('fireOnTimer');
  console.log(execSync(AIRCON_ON_COMMAND).toString());
  onTimerInfo.timeoutID =
      setTimeout(_ => { fireOnTimer(); },
                 getDiffTimeMiliSec(onTimerInfo.time, new Date()));
}
function fireOffTimer() {
  console.log('fireOffTimer');
  console.log(execSync(AIRCON_OFF_COMMAND).toString());
  offTimerInfo.timeoutID =
      setTimeout(_ => { fireOffTimer(); },
                 getDiffTimeMiliSec(offTimerInfo.time, new Date()));
}

function getDiffTimeMiliSec(timeString, nowDate) {
  var time = (parseInt(timeString.substring(0, 2)) * 60 +
              parseInt(timeString.substring(3, 5))) * 60;
  var nowTime = (nowDate.getHours() * 60 + nowDate.getMinutes()) * 60 +
                nowDate.getSeconds();
  if (time > nowTime)
    return (time - nowTime) * 1000;
  return (time + 24 * 60 * 60 - nowTime) * 1000;
}

firebase.database().ref('/timer').on('value', function(snapshot) {
  console.log('/timer: ' + JSON.stringify(snapshot.val()));
  var timer = snapshot.val();
  if (!timer)
    return;
  if (onTimerInfo.enabled) {
    clearTimeout(onTimerInfo.timeoutID);
    onTimerInfo.enabled = false;
  }
  if (offTimerInfo.enabled) {
    clearTimeout(offTimerInfo.timeoutID);
    offTimerInfo.enabled = false;
  }
  if (timer.on && timer.on.enabled && timer.on.time) {
    onTimerInfo.enabled = true;
    onTimerInfo.time = timer.on.time;
    onTimerInfo.timeoutID =
        setTimeout(_ => { fireOnTimer(); },
                   getDiffTimeMiliSec(onTimerInfo.time, new Date()));
  }
  if (timer.off && timer.off.enabled && timer.off.time) {
    offTimerInfo.enabled = true;
    offTimerInfo.time = timer.off.time;
    offTimerInfo.timeoutID =
        setTimeout(_ => { fireOffTimer(); },
                   getDiffTimeMiliSec(offTimerInfo.time, new Date()));
  }
});

あとは、node aircon.jsで起動

$ node aircon.js
43
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
49