hidao’s blog

IT系とか読書ログとか。

固定電話の着信を発信者名付きでLINE/Slack/Chatwork/Teamsに通知する仕組みを廉価につくる

ラズパイでLINE/Slack/Chatwork/Teamsに着信通知する環境が整ったのでメモ。

今回は自前で作成するプログラムの紹介をメインで行いますので、各チャットツールでプログラムからの投稿を受け付ける仕組み(Incoming Webhookなどと呼ばれる)の設定方法については割愛します。

環境

  • Raspbery Pi: Raspberry Pi4 ModelB 4GB
  • OS: Raspberry Pi OS バージョン 10
  • USBモデム: PLANEX PL-US56K2
  • 回線電話セレクター: uxcell 電話スプリッタ 延長ケーブル RJ11 6P4Cオス―2メスソケット
  • 開発言語: Node.js v10.24.0, npm 5.8.0
  • 回線種別: トーン
  • 着信通知サービスの契約: 必須

注意:パルス回線(タタタタという発信音がするもの。ピポパポ言わないやつ)だと電話を掛けるときにモデムからのノイズが入り、 電話機側から発信ができなくなります(着信はできる)。

手順

1. USBモデムの認識

1-1. USBモデムの接続

これはただUSBモデムをラズパイのUSBポートに差し込むだけ。

1-2. USBモデムのベンダーIDとプロダクトIDを調べる

lsusbコマンドを使うと現在接続されているUSB機器のリストが表示されるので、 そこからModemのチップメーカーらしき「Conexant Systems (Rockwell), Inc.」を探します。

これは今回利用するUSBモデムの一つ前のモデルを取り扱っている記事がこのメーカーだったのと、 USBSモデムを取り付ける前に確認していたときになかった行だったので、これで間違いないはず。

$ sudo lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
...何行かある
Bus 001 Device 006: ID 0572:1329 Conexant Systems (Rockwell), Inc. 
...何行かある

1-3. usbserialとして認識させる

USB接続したアナログモデムPL-US56K2をLinux上でUSBシリアルとして認識させます。
上記コマンドの結果のIDの部分の左側がベンダーIDで、右側がプロダクトIDです。

$ sudo modprobe usbserial vendor=0x0572 product=0x1329

このコマンドで「/dev/ttyACM0」のように認識されます。このまま再起動するとこの状態が失われるため、 ブート時に自動的に認識できるようsystemdを設定します。ベンダーIDとプロダクトIDは、上記コマンドの結果を使います。

[Unit]
Description=USB Serial Service
After=udev.target
After=dbus.target
After=avahi.target
After=syslog.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe usbserial vendor=0x0572 product=0x1329
ExecStop=/sbin/rmmod usbserial vendor=0x0572 product=0x1329

[Install]
WantedBy=multi-user.target

次のコマンドで実行すれば再起動しても自動的に起動するようになります。

$ sudo systemctl enable usbserial  # 再起動しても自動実行するようにする
$ sudo systemctl start usbserial   # 今すぐ起動する

2. プログラムの準備

今回はnode.jsを利用します。

2-1. node.jsのインストール

パッケージマネージャよりnode.jsnpmをインストールします。 apt install -yの利用は流儀によっては推奨されないので、ご自分の判断で。

$ sudo apt install -y nodejs npm

2-2. node.jsのパッケージの準備

今回はアナログモデムをシリアル通信で扱うため、node.jsでシリアルポートを扱うライブラリをインストールします。
さらにLINEなどの通知をするためのHTTPリクエストをするために、もう一つライブラリをインストールしておきます。

$ mkdir ~/tools
$ cd ~/tools
$ npm install serialport request

2-3. プログラムの作成

順序としては、

  1. ライブラリの読み込み
  2. シリアルポート(モデムへの経路)の初期化
  3. 電話帳の読み込み
  4. モデムのセットアップ
  5. データ受診時にチャットサービスに投稿する

という感じで行きます。

2-3-1. プログラム

npm installをしたディレクトリ内にindex.jsという名前のファイルを作り、以下のプログラムを記述します。

index.jsのプログラムを見る

/*
 *  Licence: MIT
 *
 *  Copyright 2022 hidao80
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
// 通知APIの呼び出しの準備
const request = require('request');

// シリアルポートの初期化
const SerialPort = require('serialport');
const dev = '/dev/ttyACM0';
// const dev = 'COM3'; // Windowsの場合は'COM{数字}'になる
const options = { baudRate: 115200 };
const serialPort = new SerialPort(dev, options);

// 電話帳の読み込み
const fs = require('fs');
const json = JSON.parse(fs.readFileSync('./addressbook.json', 'utf8'));
const addressbook = {};
for (index in json) { addressbook[index] = json[index].name };

/**
 * プログラム開始時に1度だけポートの初期化時にATコマンドを打ってトーン通信でナンバーディスプレイに対応させる
 * 1度実行すればモデムが状態を覚えているが、状態を確認するのも面倒なので毎回打つ
 */
serialPort.write('ATT\r\nAT+VCID=1\r\n', function(err) {
    // モデムにATコマンドを送り込んでいる
    // ATT: トーンで発信する
    // AT+VCID=1: PL-US56K2でナンバーディスプレイ信号を取得するモードに切り替える
    if (err) {
        // エラー出て上手くいかなかった場合、メッセージを表示する
        return console.log('Error on write: ', err.message)
    }
});

/**
 * データを受信したときの挙動
 */
serialPort.on('data', async function(data) {
    const input = data.toString().trim();
    if (input.indexOf('NMBR') >= 0) {
        // ナンバーディスプレイから電話番号を取得
        // こんな感じのデータが得られる: 'NMBR = 0300001111'
        const number = input.substr(7); // 先頭の7桁はいらない

        // 電話番号を取得
        if (/^\d+$/.test(number)) {
            // チャットサービスに送る着信通知のパラメータをセット
            const line = '回線1';
            let name = addressbook[`${number}`]; // 電話帳から名前を引く
            const tel  = number;
            const time = getTimestamp(); // 現在時刻を整形して使う

            if (name === undefined) {
                // 電話番号が電話帳に存在しない場合

                // フリーダイヤル相当の番号ならセールス扱い
                if (tel.indexOf("0120") == 0 ||
                    tel.indexOf("0800") == 0 ||
                    tel.indexOf("050") == 0) {
                    name = 'セールス';
                } else {
                    // 電話帳にもなく、フリーダイヤルでもなければ名前は表示しない
                    name = '';
                }
            }
            // 通知の送信
            notification(line, name, tel, time);
        }
    }
});

/**
 * ゼロ埋め2桁の日付文字列を取得する
 * @return {string} 'YYYY/MM/DD HH:mm:ss'
 */
function getTimestamp() {
    var dt = new Date();

    var year = dt.getFullYear();
    var month = ("00" + (dt.getMonth()+1)).slice(-2);
    var day = ("00" + dt.getDate()).slice(-2);

    var hour = ("00" + dt.getHours()).slice(-2);
    var min = ("00" + dt.getMinutes()).slice(-2);
    var sec = ("00" + dt.getSeconds()).slice(-2);

    return `${year}/${month}/${day} ${hour}:${min}:${sec}`;
}

/**
 * 通知を発信する
 * @param {string} line 電話回線の番号
 * @param {string} name 発信者名
 * @param {string} tel 電話番号
 * @param {string} time 着信時刻
 */
function notification(line, name, tel, time) {
    // 例としてLINE、Slack、Chatwork、Teamsへのメッセージの贈り方をすべて列挙します
    // 不要な組は削除してご利用ください

    // HTTPリクエストでLINE通知APIに送信する
    var options = {
        url: 'https://notify-api.line.me/api/notify',
        method: 'POST',
        headers: {
            'Authorization': 'Bearer <LINE Notify APIのアクセストークン>'
        },
        form: {
            'message': `【${line}】${name} tel:${tel} (${time})`
        }
    };
    // LINE通知APIへメッセージを送信する
    request(options, function (error, response, body) { })
    console.log('Send notification.');


    // HTTPリクエストでSlack Incoming WebHookに送信する
    var options = {
        url: 'https://hooks.slack.com/services/<SlackのWebhookのURL>")}}',
        method: 'POST',
        headers: {
            'Content-type': 'application/json'
        },
        form: {
            'text': `【${line}】${name} tel:${tel} (${time})`
        }
    };
    // Slackへメッセージを送信する
    request(options, function (error, response, body) { })
    console.log('Send notification.');


    // HTTPリクエストでChatworkに送信する
    var options = {
        url: 'https://api.chatwork.com/v2/room/<通知する先のルームID>/messages',
        method: 'POST',
        headers: {
            'X-ChatWorkToken': '<ChatworkのAPI tokenから取得したAPIキー>'
        },
        form: {
            'body': `【${line}】${name} tel:${tel} (${time})`
        }
    };
    // Chatworkへメッセージを送信する
    request(options, function (error, response, body) { })
    console.log('Send notification.');


    // HTTPリクエストでTeams Incoming WebHookに送信する
    var options = {
        url: 'https://{TeamsのIncoming WebhookのURL}',
        method: 'POST',
        headers: {
            'Content-type': 'application/json'
        },
        form: {
            'text': `【${line}】${name} tel:${tel} (${time})`
        }
    };
    // Teamsへメッセージを送信する
    request(options, function (error, response, body) { })
    console.log('Send notification.');
}

2-3-2. 電話帳ファイル

JSONファイルを電話帳とします。比較的コンピュータにも人間にも読みやすいファイルフォーマットだと思います。
index.jsと同じディレクトリにaddressbook.jsonという名前で保存してください。 index.jsから読み込まれます。

電話帳の書式を見る

{
    "電話番号1": {
        "name": "表示名1"
    },
    "電話番号2": {
        "name": "表示名2"
    }
}


addressbook.jsonの例を見る

{
    "0300012221": {
        "name": "Aさま"
    },
    "09011112222": {
        "name": "Bさん"
    },
    "05011113333": {
        "name": "Cさん"
    },
    "012011114444": {
        "name": "【セールス】〇〇電力"
    }
}

3. 実行

着信の待受を開始するには次のコマンドを実行します。

$ node index.js

このプログラムの欠点は、プログラムを実行している仮想端末を閉じると着信通知プログラムも終了してしまうことです。 解決する方法もありますが、ここでは割愛させていただきます。

さいごに

各チャットツールへの通知を送信するAPIを呼び出すプログラムの書き方はバージョンアップされてここに掲載したとおりでは動かなくなることが予想されます。
また、LINE通知API以外は各チャットツールのAPIリファレンスなどを参考に作成したもので、動作検証は行っていません。あしからず。

余談ですが、node.jsが用意できさえすればWindowsでも同じプログラムが動作します。
ラズパイはないけど、使ってないパソコンがあるよ! という方はWindowsパソコンで動かすのも良いかもしれません。

もうちょっとしっかり組めば簡易的なCTIシステムが組めそうですね。