Raspberry Piで自宅にかかってきた電話番号をLine通知してみた

:::note warn
新しく機能追加して作り直した内容をZenn無料本で公開しています。
https://zenn.dev/haradaj/books/a838c6c686c2a7
:::

年末のAmazonサイバーマンデーにのせられて、RaspberryPiキットをポチってしまったのです。せっかくなので、何かに使えないかということで、冬休みの宿題に取り組みました。

うちは共働きなので、家の固定電話にかかってきた電話が全部留守番電話に溜まり、気が付くと10件以上になっていることも。電話がかかってきたときに通知してくれれば、必要に応じて折り返しできるなということで、家電にかかった電話をLine通知するようにしてみました。

#環境や前提条件
- Raspberry Pi 3 Model b+：アマでポチってしまったやつ。実際はキットで買いました。[ABOX Raspberry Pi 3 Model b+ ラズベリーパイ 3 b＋【2018新型】](https://www.amazon.co.jp/gp/product/B07FQ9678G/ref=oh_aui_detailpage_o05_s00?ie=UTF8&psc=1)
- 光電話：うちはBiglobe光電話ですが、NTT回線の光電話で借りている宅内ルータがPR-400KIというやつでした

#宅内ルータの設定
宅内ルータにログインします。LAN側からアクセスする場合は、192.168.1.1あたりのアドレスにブラウザでアクセスすると設定画面が開きます。
![ルータ１.PNG](https://qiita-image-store.s3.amazonaws.com/0/48177/977f5989-5eaf-11ae-c01c-ad27de2a2dd4.png)

ログインしたら、左のメニューで「電話設定」→「内線設定」を開きましょう。
![ルータ２.PNG](https://qiita-image-store.s3.amazonaws.com/0/48177/32197dc5-3f5b-bb21-8b7c-76eb914417a3.png)

最初は、固定電話がつながっている内線しか、一番左の「利用有無」にチェックがない状態と思います。うちでは、内線番号１しか有効ではありませんでした。
ここで、IPphoneのうち一つを有効化します。チェックボックスをチェックして、画面下の設定ボタンを押しましょう。
最初は、「端末属性」「登録状態」「IPバージョン」などはこの画面と違うと思いますが、設定するとあっていきますので、気にしないで進みましょう。
有効にしたら、右から二番目の編集ボタンを押して、詳細を設定していきます。

![ルータ３.PNG](https://qiita-image-store.s3.amazonaws.com/0/48177/f0b548f0-0ae2-b926-8cf5-343408f1531c.png)
ここで変更するのは「端末属性」を”音声専用端末”に、「ダイジェスト認証」を”行う”に、さらに「パスワード」を強固なパスワードに変更します。私は万万が一外部から入り込まれたりすると電話を悪用される可能性があるため、ランダム文字列を生成して設定しました。

また、ここで設定されている以下の項目は、後程Asteriskの設定で使いますので、メモっておきましょう。

- 内線番号
- ユーザID
- パスワード

設定したら、更新ボタンが下のほうにあるので忘れず押しましょう。

# Raspberry Piにアスタリスクをインストール

Asterisk全般については、主にこちらのサイトで勉強させてもらいました。
- [VoIp-Info.jp](https://voip-info.jp/)
- [Asteriskを使う---目次](https://tech.nikkeibp.co.jp/it/article/COLUMN/20070117/258890/)

## 必要なもののインストール
でも、実際Raspberry PiへのAsteriskインストールは、パッケージで行ったので、以下のコマンド一発
`$ sudo apt-get install asterisk`
Pythonで着信を処理するスクリプトを書くのでAsteriskのPythonライブラリであるpystも入れておきましょう。
また、着信をいったんsqllite3のテーブルに格納しますので、そちらもインストールします。
`$ sudo apt-get install python-pyst sqlite3`

## Asteriskの設定
/etc/asterisk の下に設定ファイルができるのですが、不要なものもいろいろサンプル的に置かれているので、いったんバックアップして必要なものだけ置くようにします。

```shell-session:設定ファイルをバックアップ
$ cd /etc
$ sudo mv asterisk asterisk.bak
$ sudo mkdir asterisk
$ sudo chown asterisk:asterisk /etc/asterisk
```

これで空っぽなディレクトリができました。

最低限必要な設定ファイルだけを/etc/asteriskに置いていきます。設定ファイルは、以下のサンプルを基にさせていただきました。
[Asterisk 13 サンプル設定ファイル](https://voip-info.jp/index.php/Asterisk_13_%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)
こちらのファイルをダウンロードして、空のフォルダに解凍後、以下のファイルを残して不要なものは削除します。

```shell-session:/etc/asteriskの中
$ ls -l
-rw-r----- 1 asterisk asterisk  241  7月 26  2006 asterisk.conf
-rw-r----- 1 asterisk asterisk  365 12月 31 12:34 extensions.conf
-rw-r----- 1 asterisk asterisk   10  3月  6  2016 features.conf
-rw-r----- 1 asterisk asterisk 1211  7月 26  2006 logger.conf
-rw-r----- 1 asterisk asterisk  566  1月  2 14:57 modules.conf
-rw-r----- 1 asterisk asterisk  747  1月  2 14:53 sip.conf
```

まず、sip.confを編集します。

```conf:sip.conf
[general]
maxexpirey=3600
defaultexpirey=3600
context=default
;SIPポートは5060,PJSIPとの被りに注意！
bindport=5060
bindaddr=0.0.0.0
srvlookup=no
;OpenGateの場合allowguestはno
allowguest=no
disallow=all
allow=ulaw
allow=alaw
allow=gsm
language=ja

register => <内線番号>:<パスワード>:<ユーザID>@<宅内ルータLAN側IPアドレス>/<外線電話番号>

[ntttel]
type=friend
secret=<パスワード>
username=<ユーザID>
fromuser=<内線番号>
host=<宅内ルータLAN側IPアドレス>
contexet=default
insecure=port,invite
dtmfmode=inband
canreinvite=no
disallow=all
allow=ulaw

;ACL
;安全措置のため192.168系以外は受け付けない
;他のネットワークを使用している場合にはここを変更すること
deny=0.0.0.0/0
permit=192.168.1.0/255.255.255.0
```

register行のところは、上で宅内ルータを設定した際にメモった内線番号、ユーザID、パスワードを使います。この設定で、宅内ルータに対してIP電話としてAsteriskサーバが登録されるようになります。

次は、extensions.confです。

```conf:extensions.conf
[general]
writeprotect=no
priorityjumping=no

[globals]
;自局の着信番号を設定する
;[incoming]セクションを参照
;ひかり電話HGWの番号(着信番号:自分の番号)
MYNUMBER1=<外線電話番号>

[default]
exten => ${MYNUMBER1},1,NoOp(${CALLERID(all)})
exten => ${MYNUMBER1},2,Log("NOTICE",${CALLERID(all)})
exten => ${MYNUMBER1},3,AGI(incoming.py)
```

[default]セクションのところが、dialplanと呼ばれる着信時の動作を記述するところです。ここでは3行ありますが、実際は3行目のアクションのみが外部スクリプトを呼び出すために必要なものです。

dialplan行の右側は
「電話番号」，「行番号」，「アクション」
の構成で、アクションにあるコマンドはそれぞれ以下の意味があります。

- NoOp：何もしない。デバッグ時にコンソール表示で確認するために記載
- Log：ログ出力する
- AGI：AsteriskGatewayInterface 外部スクリプトを呼び出す。ここでは"incoming.py"を呼び出しています

そのほかの設定ファイルは、サンプルそのままか、ファイルがないとエラーとなるものなどを置いています。一応内容を記載しておきます。

```conf:asterisk.conf
[directories]
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/spool/asterisk
astrundir => /var/run
astlogdir => /var/log/asterisk
```

```conf:features.conf
[general]
```

```conf:logger.conf
[general]

[logfiles]
console => notice,warning,error
messages => notice,warning,error
```

```conf:modules.conf
[modules]
autoload=yes
;
noload => pbx_gtkconsole.so
noload => pbx_kdeconsole.so
;
noload => app_intercom.so
;
noload => chan_mgcp.so
noload => chan_skinny.so
noload => chan_phone.so
noload => chan_unistim.so
noload => chan_modem.so
;
noload => chan_modem.so
noload => chan_modem_aopen.so
noload => chan_modem_bestdata.so
noload => chan_modem_i4l.so
noload => chan_misdn.so
;
noload => res_musiconhold.so
;
;ALSAを使用していない場合には以下をnoloadに
noload => chan_alsa.so
noload => chan_oss.so
;
noload => app_adsiprog.so
[global]
chan_modem.so=no
```

設定ファイルが置けたらAsteriskサービスを起動します。パッケージを導入した際に起動している場合もあるため、現在のステータスを確認します。

```shell-session
$ sudo systemctl status asterisk.service
● asterisk.service - Asterisk PBX
   Loaded: loaded (/lib/systemd/system/asterisk.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2019-01-02 14:59:44 JST; 1 day 18h ago
     Docs: man:asterisk(8)
 Main PID: 16412 (asterisk)
   CGroup: /system.slice/asterisk.service
           mq16412 /usr/sbin/asterisk -g -f -U asterisk
```

ステータスが(running)で既に起動しているようですので、再起動しておきます。
`$ sudo systemctl restart asterisk.service`

# スクリプトを配置する

外線が着信したときに上記の設定の通り、incoming.pyが起動されます。その中でLineへ通知できればよかったのですが、Asteriskから起動する場合のPythonのバージョンは2.7になり、その中ではLineMessagingAPIがうまく動かなかったので、いったんSQLite3のDBに情報を格納して、別途Line通知はCron起動で出すようにしました。

## 着信時のスクリプトの準備

スクリプト類はasterisk.confに記載されているastagidir に配置します。ここでは、/var/lib/asterisk/agi-binに置きます。

```python:incoming.py
#!/usr/bin/python
"""
Example to get and set variables via AGI.
You can call directly this script with AGI() in Asterisk dialplan.
"""
from datetime import datetime
import sqlite3
from asterisk.agi import *

agi = AGI()

uniqueId = agi.env['agi_uniqueid']
callerId = agi.env['agi_callerid']
callerIdName = agi.env['agi_calleridname']
dt = datetime.now().isoformat()

conn = sqlite3.connect('tel_line.sqlite3')
cur = conn.cursor()
cur.execute("insert into incoming values (?, ?, ?, ?, ?)", (uniqueId, callerId, callerIdName, dt
, None))
conn.commit()
conn.close()

sys.exit()
```

このスクリプトでは、外線がかかったときにその情報をSQLite3でDBに格納します。そのため、DBを作っておきましょう。
スクリプトが起動するディレクトリ/var/lib/asteriskに移動して、以下のようなsqlファイルを作っておきます。

```sql:createtable.sql
create table incoming
(
        uniqueid        text primary key,
        callerid        text,
        calleridname    text,
        datetime        text,
        pushed          text
);
```

このファイルで、DBとテーブルを作っておきます

```shell-session
$ cd /var/lib/asterisk
$ sudo sqlite3 tel_line.sqlite3
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> .read createtable.sql
sqlite> .quit
$ chown asterisk:asterisk tel_line.sqlite3
$ ls -l tel_line.sqlite3
-rw-r--r-- 1 asterisk asterisk 12288  1月  5 09:35 tel_line.sqlite3
```

これで、電話をかけるとこのテーブルにレコード追加されるはずです。

```shell-session:レコード追加の確認
$ sudo sqlite3 tel_line.sqlite3
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from incoming;
1546227317.9|<着信電話番号>|<着信番号名>|2018-12-31T12:35:17.473582|2019-01-01T21:14:32.724778
```

## Line通知スクリプトの配置

Line通知は、MessagingAPIのPUSHを使います。LineBotの準備は、以下のページなどを参考にしてください。ただ、今回はメッセージのPUSHのみなので、Webhookは、いりません。

以下のようなスクリプトを、上記と同じく/var/lib/asterisk/agi-binに置きます。

```python:tel_line.py
#!/usr/bin/python3
import sys
import sqlite3
from datetime import datetime
from linebot import LineBotApi
from linebot.models import TextSendMessage
from linebot.exceptions import LineBotApiError

line_bot_api = LineBotApi(<アクセストークン（ロングターム）>)

dbpath = '/var/lib/asterisk/tel_line.sqlite3'
conn = sqlite3.connect(dbpath)
cur = conn.cursor()

rows = cur.execute('SELECT * FROM incoming WHERE pushed IS NULL')
rows = rows.fetchall()
for row in rows:
    tel_message = "電話がありました：" + row[1]

    try:
        line_bot_api.push_message(<Your user ID>,TextSendMessage(text=tel_message))
        cur.execute('UPDATE incoming SET pushed=? WHERE uniqueid=?', (datetime.now().isoformat(), row[0]))
    except LineBotApiError as e:
        print("error occured!")
        print(e.status_code)
        print(e.error.message)
        print(e.error.details)
        sys.exit()

conn.commit()
conn.close()

sys.exit()
```

<アクセストークン（ロングターム）>と<Your user ID>については、LINE Developersサイトでチャネルを作ったときに取得したものに置き換えてください。

# 動作確認

上記でうまくいけば電話があったときに、Lineに通知が来るはず。
<img src="https://qiita-image-store.s3.amazonaws.com/0/48177/301e574d-f2ff-671a-e002-4d9b00987340.jpeg" width=300px>

# 最後に

何とかここまでできたのですが、電話番号だけだと誰からかわからないので、その辺も改良したいですね。
あと、留守電自体をAsteriskで受けて、外でメッセージ自体聞けるようにならないかなとか。