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

# いきさつ

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

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

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

の２通りが考えられるけど、半田ごてが見当たらなかったので、キットを買ってくることにした。ネットで調べると[USB接続 赤外線リモコンキット(株式会社ビット・トレード・ワン)](http://bit-trade-one.co.jp/BTOpicture/Products/005-RS/index.html)、[irMagician(大宮技研 合同会社)](http://www.omiya-giken.com/?page_id=837)辺りが、Raspberry Piでの実績がすでにあるらしい。irMagicianの方がメモリ容量も大きいので長い信号に対応してそうな上、サイズも小さいので、irMagicianを使うことにした。

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

![aircon.png](https://qiita-image-store.s3.amazonaws.com/0/110601/11561037-9be6-1f30-0175-9833a2275a5b.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プラザ](http://www.itplaza.co.jp/)で完成品を購入。(はんだ付けに自信があれば1,980円のキットのほうがお得。)それ以外は[秋月電子](http://akizukidenshi.com/catalog/default.aspx)で購入。Raspberry Pi 2 Model Bをすでに持ってるけど、Raspberry Pi 3の方がWi-FiとBluetooth(今回は使わない)を内蔵してるし、せっかくなので、と思い衝動買い。

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


# セットアップ
## Macで起動用SDカードの作成
[公式ページ](http://www.raspberrypi.org/downloads/)から最新のRASPBIAN JESSIE LITEをダウンロード。（12月4日現在2016-11-25版、2016-11-25-raspbian-jessie-lite.zipが最新）

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


```bash
$ 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`で何バイト書き込まれたか確認できる。

```bash
$ 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 Options` → `I2 Change Timezone` → `Asia` → `Tokyo`
- `4 Internationalisation Options` → `I3 Change Keyboard Layout` → `Generic 105-key (Intel) PC` → `Other` → `Japanese` → `Japanese` → `The default for the keyboard layout` → `No compose key`
- `4 Internationalisation Options` → `I4 Change Wi-fi Country` → `JP Japan`
- `7 Advanced Options` → `A4 SSH` → `Yes`

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

```filename: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 update`と`sudo apt-get dist-upgrade`でパッケージの[更新](https://www.raspberrypi.org/documentation/raspbian/updating.md)をして初期設定は終了。

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

```filename: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プログラム例が[公開されている](http://www.omiya-giken.com/?page_id=1231)。このプログラムはpyserialというパッケージを使っているので、まず、pipをインストールしてから、pyserialをインストールする。

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

`cap.py`、`play.py`、`saveIrMagician.py`、`playIrMagicianLocal.py`をダウンロード。

```bash
$ 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](https://console.firebase.google.com/)にログインし、「新規プロジェクトを作成」をクリック。
適当なプロジェクト名を入力して、「プロジェクトを作成」をクリック。
ギアのアイコンをクリックし、「プロジェクトの設定」をクリックし、「プロジェクト ID」をメモっておく。

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

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

```javascript
  // 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をインストールしておく必要がある。)

```bash
$ npm install -g firebase-tools
```

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

```bash
$ 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を下記のように変更。

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

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

```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`でデプロイ。

```bash
$ firebase deploy
```

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

# Raspberry PiにNodeJSをインストール

```bash
$ 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に繋いでリモコンを操作するプログラム

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

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


```js: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`で起動

```bash
$ node aircon.js
```