RaspberryPiで「ほんわか お知らせランプ」Part 3

## RaspberryPiで監視をお助け
このシリーズでは、RaspberryPiを使ったお知らせランプを製作します。
![Blue.GIF](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/Blue.GIF)

第３回の今回は、「お知らせランプ」の全体制御を行うメイン部分です。
このシリーズは、今回で完成となります。

![SystemDiagram2.png](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/SystemDiagram3.png)

## Abu（アブー）登場
Detector経由でOpsGenieのAlert情報を取得して、監視対象の状態を特定し、LED表示を設定する仕事は、Alert Buster(略してAbu)が担います。
新たなAlertが発生した時に一度だけチャイムを鳴らすのもAbuの仕事です。

同時に複数存在するかもしれないAlertの状態に対し「お知らせランプ」が表示できるのは一つの状態のみ。
そこで、状態を優先度付けして、最も優先度の高い状態を表示します。

## Abuが扱う状態と表示の関係

Abuが扱う状態は、以下の６種類です。
レベルは「エラー」と「ワーニング」の２種類。
「エラー」は急いで対処するもの、「ワーニング」は急ぎではないけど、確認が必要なものを表します。
それぞれ、認知済みかどうかの２状態があります。

「不明」は、起動直後やネットワークの不具合などで、状態がわからない時に使用します。
各状態に割り当てるべき色や、明滅周期も決めておきます。
なお、"Magenta"などの色の名称は、前回の記事のLed.COLORSで定義しているものを使います。

| 優先度 | Alert種別 | 認知状態 | 状態値 | 色 | 明滅周期 | メモ |
|---:|---|---|---:|---|---|---|
| 1 | 不明 | - | 1 | Magenta | 0（明滅なし） |  |
| 2 | エラー | 未認知 | 2 | Red | 0.5秒 |  |
| 3 | ワーニング | 未認知 | 4 | Yellow | ２秒 |  |
| 4 | エラー | 認知済み | 8 | Green | ２秒 |  |
| 5 | ワーニング | 認知済み | 16 | Green | ６秒 |  |
| 6 | Newエラー | - | 32 | - | - | チャイム用 |
| 7 | Newワーニング | - | 64 | - | - | チャイム用 |
| 8 | 正常 | - | 0 | Blue | ８秒 |  |

Abuはこれらの状態を単一の整数値「状態値」として扱います。
「状態値」は、ビット演算で扱いやすい様に、２進数の各ビット値を割り当てます。
「正常」は、全部のビットが0の状態なので、bit割り当てはありません。

内部では、上表のチャイム制御用のフラグも合わせた7bitのbit単位の論理和を一つの状態値として使用します。
例えば、まだ認知されていない新たなエラー（チャイムを鳴らす前）がある場合の状態値は、0x22です。

>状態値はbit毎に意味をもたせた単一の整数値で表す方式としましたが、可読性を重視してPythonのSet型を使っても良いでしょう。

## チャイム

### 鳴らし方
新たなエラーやワーニングが見つかった時は、気付きやすいように一度だけチャイムを鳴らします。
チャイムは、サウンドファイルとして用意したものを、aplayコマンドに渡して再生します。

``` bash:
$ aplay --quiet /usr/share/sounds/alsa/Rear_Center.wav
```

あらかじめ、本体のイヤホンジャックもしくは、USBポートにスピーカを接続して、音が鳴るように設定しておきます。
参考：https://www.google.co.jp/search?q=raspberrypi+aplay

### 音源
チャイムの音源は、aplayがサポートする形式ならなんでもOKです。
Abuでは[freesound.org](http://www.freesound.org)より以下を使いました。
ユーザ登録するとダウンロード可能になりますので、ライセンスを確認して使用します。

- Warning
気付くことができれば良いので、控えめな音を選びました。
112860__paulnorthyorks__eighties-synth.wav
http://www.freesound.org/people/paulnorthyorks/sounds/112860/

- Error
緊迫感を感じる音を選びました。
以下の音声を加工して、３回繰り返す音源を作成し、使用しています。
204425__jaraxe__alarmx3.wav
http://www.freesound.org/people/JarAxe/sounds/204425/
加工には、[Audacity](http://ja.wikipedia.org/wiki/Audacity)を使いました。

## Abuの動作
Abuの仕事は以下の通りです。

- Detectorを初期化
- LED Deamonを起動
- 以下を繰り返す
  - Detectorを呼び出して、Alertのリストを取得する
  - Alertリストをまとめて、状態値を作成
  - 状態値に応じて、LED表示を設定
  - 新たなエラーやワーニングがあれば、チャイムを鳴らす
  - 1分間Sleep

## 起動方法

```bash
$ ./abu.py ********-****-****-****-************
```
```********-****-****-****-************```の部分には、前回の記事で紹介した、OpsGenieのAPI Keyを指定します。
OS起動時に自動起動するように、cronに登録する場合は、例えば以下のようにcronに追記すると良いでしょう。

```bash
$ crontab -e
@reboot cd path-to-module-dir;./abu.py ********-****-****-****-************ >/dev/nul 2>&1
```
## ソースコード

```python:abu.py
#!/usr/bin/env python
#coding:utf-8


import time
import os
from opsgenie import OpsGenie
from led_deamon import LedDeamon


class Abu(object):
    DOWN = 1
    ERR_UNACKED = 2
    WARN_UNACKED = 4
    ERR_ACKED = 8
    WARN_ACKED = 16
    ERR_NEW = 32
    WARN_NEW = 64
    NORMAL = 0
    ERROR_SOUND = "./204425__jaraxe__alarm-2.wav"
    WARNING_SOUND = "./112860__paulnorthyorks__eighties-synth.wav"

    STATE_MAP = {
        DOWN: {"color": "Magenta", "interval": "0"},
        ERR_UNACKED: {"color": "Red", "interval": "0.5"},
        WARN_UNACKED: {"color": "Yellow", "interval": "4"},
        ERR_ACKED: {"color": "Green", "interval": "4"},
        WARN_ACKED: {"color": "Green", "interval": "8"},
        NORMAL: {"color": "Blue", "interval": "8"}}

    def _state2mode(self, state):
        if state == Abu.NORMAL:
            return Abu.STATE_MAP[Abu.NORMAL]
        for s in (Abu.DOWN, Abu.ERR_UNACKED, Abu.WARN_UNACKED,
                  Abu.ERR_ACKED, Abu.WARN_ACKED):
            if state & s:
                return Abu.STATE_MAP[s]
        else:
            return Abu.STATE_MAP[Abu.NORMAL]
            return None

    def __init__(self, url, api_key):
        self.opsgenie = OpsGenie(url, api_key)

    def _summarize(self, list):
        state = Abu.NORMAL
        if list is None:
            state = Abu.DOWN
        else:
            for l in list:
                if l["error"]:
                    if l["acknowledged"]:
                        state |= Abu.ERR_ACKED
                    else:
                        state |= Abu.ERR_UNACKED
                        if l["new"]:
                            state |= Abu.ERR_NEW
                else:
                    if l["acknowledged"]:
                        state |= Abu.WARN_ACKED
                    else:
                        state |= Abu.WARN_UNACKED
                        if l["new"]:
                            state |= Abu.WARN_NEW
        return state

    def start(self):
        ld = LedDeamon()
        ld.set_mode(self._state2mode(Abu.DOWN))
        ld.start()
        while True:
            alert_list = self.opsgenie.detector()
            # print alert_list
            state = self._summarize(alert_list)
            # print hex(state)
            mode = self._state2mode(state)
            if mode:
                ld.set_mode(mode)
            if state & Abu.ERR_NEW:
                os.system("aplay " + Abu.ERROR_SOUND + "&")
            else:
                if state & Abu.WARN_NEW:
                    os.system("aplay " + Abu.WARNING_SOUND + "&")
            time.sleep(60)

if __name__ == '__main__':
    import sys

    if len(sys.argv) != 2:
        print("Usage: %s 'api-key for your OpsGenie account'." % sys.argv[0])
        print("Example: %s ********-****-****-****-************." % sys.argv[0])
        print("You can get your OpsGenie account at https://www.opsgenie.com.")
        exit()
    apiKey = sys.argv[1]
    a = Abu(apiKey)
    a.start()
```

## 振り返り

Pythonの勉強方々RaspberryPiを使ってみました。
安価でPowerfulそしてGPIOを手軽に使えるライブラリもあり、こうした用途にはとてもマッチしていることがわかりました。
PWMによるLEDのカラー制御、非同期処理、状態値の取り扱い、OpsGenie連携など、いくつか悩むポイントはありましたが、幸いにして、それぞれ無理のない解決策を見出すことが出来、完成に至ることができました。
今回製作した「お知らせランプ」は、職場の雰囲気にもあまり違和感なく溶け込んで、実運用でも役立っています。
興味のある方はお試しになってみてください。

このシリーズは、この記事で完結となりますが、次のアイディアが固まったら、また投稿しようと思います。