ポンコツエンジニアのごじゃっぺ開発日記。

いろいろポンコツだけど、気にするな。エンジニアの日々の開発などの記録を残していきます。 自動で収入を得られるサービスやシステムを作ることが目標!!

【SORACOM SIM】LINE Botを使ってsimをオン/オフして料金を節約できるiPhoneにしてみた。

だいぶ前(2年位前)の話にはなってしまうのですが、職場の人にiPhone6sをいただきました。そこで使うSIMをソラコムにしてみたので、いろいろAPIを叩いて遊んでみました。

soracom.jp

SIM検討

まずは、ソラコムまでにたどり着くまでに、他の格安SIMとかとも悩んでいたのですが、結構職場の人もSORACOMのSIMを使っている人も多く、おすすめされたので、購入してみることにしました。

SORACOMは、IoT/M2M向けワイヤレス通信を提供するプラットフォームです。セルラー、LPWA(LoRaWAN、Sigfox)を、1回線からリーズナブルにご利用いただけます。それらの通信は、ウェブコンソールやAPIを通じて一括操作・管理することができます。さらにプラットフォームには、IoTシステム構築に必要となる、セキュリティやデバイス管理、クラウド連携などのサービスが用意されており、これらを必要に応じて組み合わせて利用することで、少ないリソースでスピーディにIoTシステムを構築し、ビジネス活用を始めることができます。

f:id:ponkotsu0605:20200426150634j:plain

ということで、購入したのがSORACOM Air SIMです。

SORACOMのいいところは、APIが使えるところです。 APIリファレンスは以下のリンクです。

dev.soracom.io

APIで何ができるかというと、通信のON/OFFができるということ。すなわち、使わないときはOFFにして、使うときだけONにするということができるようになって、費用も最低限に抑えられます。 ※ちなみに従量課金制になってますので、通信した分だけお金がかかるSIMです。

基本料金は10円/日+使った分だけ、なので、月300円から使えることになります。

こちらのsimはamazonからも購入可能になっています。

LINEのBotとGASを用いたシステム構成

今回のシステム構成としては、LINEのMessaging APIを使います。 サーバサイドは無料で使えるGoogle Apps Script(GAS)を使います。

GASのスクリプト

まずはLINEのwebhookを受け取るところの処理です。 自分はいつもこの形を使っていますので参考になれば使って下さい。

gas.gs

// pushしたいときに送る先のuser_idで、自分の値はスプレッドシートに残るログを見ればわかる
var log_user_id = '********************';
// postされたログを残すスプレッドシートのid
var spreadsheet_id = '**********************';


/**
 * postされたときの処理
 */
function doPost(e) {
  try {
    var json = JSON.parse(e.postData.contents);
    SpreadsheetApp.openById(spreadsheet_id).getSheetByName('log1').getRange(1, 1).setValue(json.toSource());
//    SpreadsheetApp.openById(spreadsheet_id).getSheetByName('log1').getRange(2, 1).setValue(json.events[0].source.groupId.toSource());
    
    switch(json.events[0].type) {
      case 'join':
        push(log_user_id, 'join');
        break;
      case 'message':
        getMessage(json);
        break;
      case 'postback':
        getPostback(json)
        break;;
      default:
        push(log_user_id, 'default');
        push(log_user_id, e.postData.contents);
        break;
    }
  } catch (ex) {
    push(log_user_id, 'exception\n' + json.events[0].toSource());
    push(log_user_id, ex.message);
  }
}

次にどこのグループ(orユーザ)から来たデータなのかを判別するところです

getMessage.gs

/**
 * メッセージを受け取ったときの処理
 * userかgroupかを分けるだけ
 */
function getMessage(json) {
  switch(json.events[0].source.type) {
      // 1対1メッセージ
    case 'user':
      getUserMessage(json);
      break;
      // グループチャット
    case 'group':
      getGroupMessage(json);
      break;
  }
}

/**
 * userチャットを受け取ったときの処理
 */
function getUserMessage(json) {
  var userId = json.events[0].source.userId;
  var text = json.events[0].message.text;
  var replyToken = json.events[0].replyToken;
  switch(userId) {
    default:
      push(log_user_id, '見知らぬ人チャット');
      break;
  }
}

/**
 * groupチャットを受け取ったときの処理
 */
function getGroupMessage(json) {
  var groupId = json.events[0].source.groupId;
  var text = json.events[0].message.text;
  var replyToken = json.events[0].replyToken;
  switch(groupId) {
      // あいほん
    case '*******************':
      getSoracomMessage(replyToken, groupId, json.events[0].message);
      break;
    default:
      push(log_user_id, 'その他グループ:' + groupId);
      break;
  }
}

Postbackも同じような処理を書きます

getPostback.gs

/**
 * メッセージを受け取ったときの処理
 * userかgroupかを分けるだけ
 */
function getPostback(json) {
  switch(json.events[0].source.type) {
      // 1対1メッセージ
    case 'user':
      getUserMessage(json);
      break;
      // グループチャット
    case 'group':
      getGroupPostback(json);
      break;
  }
}
function getUserPostback(json) {}
function getGroupPostback(json) {
  var groupId = json.events[0].source.groupId;
  var postback = json.events[0].postback;
  var replyToken = json.events[0].replyToken;
  
  var data = {};
  var pair=postback.data.split('&');
  for(var i=0;pair[i];i++) {
    var kv = pair[i].split('=');
    data[kv[0]] = kv[1];
  }
  
  switch(groupId) {
      ///// あいほん
    case '********************':
      getSoracomPostback(replyToken, groupId, postback);
      break;
      
      ///// 何もしない
    default:
      push(log_user_id, 'その他グループ postback:' + groupId + '\n' + postback.toSource());
      if (postback.data && 'message' in data) {
        push(groupId, data.message)
      }
      break;
  }
}

次に今回のメインの処理です。 SORACOMのAPIを叩いたりするところです。

soracom.gs

// 管理画面から取得するキーなど
var soracomAuthKeyId = 'keyId-*******************';
var soracomAuthKey = 'secret-*******************';
var soracomLineGroupId = '********************';
var soracomName = 'iPhone6s'; // tagの名前を入力する

var soracomImsi = null;
var soracomApiKey = null;
var soracomToken = null;


/**
 * LINEに投稿されたら呼ばれる
 * 接続状況に応じてpostbackを投稿する
 */
function getSoracomMessage(replyToken,groupId, message) {
  var subscriberData = getSoracomsubscribers();
  if (subscriberData.status == 'active') {
    push(soracomLineGroupId, '現在は接続されています');
    pushSoracomInactiveButtons();
  } else {
    push(soracomLineGroupId, '現在は切断されています');
    pushSoracomActiveButtons();
  }
}

/**
 * LINEからpostbackのボタンを押されたら呼ばれる
 * 選択されたボタンによって挙動を変える
 */
function getSoracomPostback(replyToken, groupId, postback) {
  soracomInitialize();
  switch(postback.data) {
    case 'offline':
      soracomDeactivate();
      push(groupId, '切断したお');
      break;
    case 'change':
      pushSoracomActiveButtons();
      break;
    default:
      soracomActivate();
      soracomUpdateSpeedClass(postback.data);
      push(groupId, postback.data + 'にしたお');
      break;
  }
}

/**
 * soracomのapiを使う前に呼ぶ
 * これを呼べばapiが使えるようになる
 */
function soracomInitialize() {
  var data = getSoracomsubscribersByName(soracomName);
  if (!data) throw new Exception('指定した端末が見つかりませんでした');
  soracomImsi = data.imsi;
  soracomAuthorize();
}

/**
 * soracomのapiを叩くためのトークンを取得する
 */
function soracomAuthorize() {
var url = 'https://api.soracom.io/v1/auth';
  var headers = {
  };
  var postData = {
  'authKeyId': soracomAuthKeyId,
  'authKey': soracomAuthKey,
  }
  var options = {
    "method" : "post",
    'contentType': 'application/json',
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  soracomToken = result.token
  soracomApiKey = result.apiKey;
}

/**
 * simをアクティブにする(接続)
 */
function soracomActivate() {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/activate';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * simを非アクティブにする(切断)
 */
function soracomDeactivate() {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/deactivate';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * スピードのクラスを変更する
 */
function soracomUpdateSpeedClass(speedClass) {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/update_speed_class';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var postData = {
    "speedClass": "s1." + speedClass
  }
  var options = {
    "method" : "post",
    'contentType': 'application/json',
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * 指定したtag名のsubscriberを取得する
 */
function getSoracomsubscribersByName(name) {
  soracomAuthorize();
  var url = 'https://api.soracom.io/v1/subscribers'
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var options = {
    "method" : "get",
    'contentType': 'application/json',
    "headers" : headers,
  };

  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  for (var i = 0; i < result.length; i++) {
    if (result[i].tags.name == name) {
      return result[i];
    }
  }
  Logger.log('getSoracomsubscribersByName:Not Found.');
  return null
}

var _soracomSubscriberData = null;
/**
 * 使用するsimのsubscriberを取得する
 */
function getSoracomsubscribers() {
  if (_soracomSubscriberData) return _soracomSubscriberData;
  soracomInitialize();
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi;
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var postData = {

  }
  var options = {
    "method" : "get",
    'contentType': 'application/json',
    "headers" : headers,
  };
  
  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  _soracomSubscriberData = result;
  
  return result;
}

/**
 * 非アクティブ化するボタンを送信する
 */
function pushSoracomInactiveButtons() {
  var groupId = soracomLineGroupId;
  var subscriberData = getSoracomsubscribers();
  pushButtons(
    groupId,
    'https://corporate-tech-blog-wp.s3.amazonaws.com/tech/wp-content/uploads/2015/10/fig_home_02.png',
    'iPhone6s[' + subscriberData.speedClass + ']',
    '通信を切断しますか?',
    [
      {
        "type": "postback",
        "label": "切断する",
        "data": "offline"
      },
      {
        "type": "postback",
        "label": "classを変更する",
        "data": "change"
      },
    ]);
}

/**
 * アクティブ化するボタンを送信する
 */
function pushSoracomActiveButtons() {
  var groupId = soracomLineGroupId;
  var subscriberData = getSoracomsubscribers();
  pushButtons(
    groupId,
    'https://corporate-tech-blog-wp.s3.amazonaws.com/tech/wp-content/uploads/2015/10/fig_home_02.png',
    'iPhone6s[' + subscriberData.speedClass + ']',
    'どのクラスで接続しますか?',
    [
    
    {
    "type": "postback",
    "label": "s1.minimum",
    "data": "minimum"
    },
    {
    "type": "postback",
    "label": "s1.slow",
    "data": "slow"
    },
    {
    "type": "postback",
    "label": "s1.standard",
    "data": "standard"
    },
    {
    "type": "postback",
    "label": "s1.fast",
    "data": "fast"
    },
    ]);
}


/**
 * 毎日切断する。
 * このメソッドを未明(4時~5時)に呼ぶように設定する。
 */
function soracomAutoOffline() {
  soracomInitialize();
  var subscriberData = getSoracomsubscribers();
  if (subscriberData.status == 'active') {
    soracomDeactivate();
    push(soracomLineGroupId, '夜中なので切断するお');
  }
}

/**
 * 料金を報告する
 * このメソッドを毎日呼ぶように設定する
 */
function soracomGetBillsLatest() {
  soracomInitialize();
  var url = 'https://api.soracom.io/v1/bills/latest';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "get",
    "headers" : headers,
  };

  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText()).amount;
  push(soracomLineGroupId, '最新の料金は' + result + '円だお');
}

LINEの送信する周りの処理をまとめたもの。 今回使ってないメソッドもたくさんこちらに書いてあるので、ぜひ使ってみて下さい。

line.gs

// line developersに書いてあるChannel Access Token
var access_token = '**********************';

/**
 * 指定のuser_idにpushをする
 */
function push(to, text) {
  Logger.log('push:' + to + ':' + text)
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}


/**
 * 指定のuser_idにpushをする
 */
function pushImage(to, src, srcPreview) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'image',
        'originalContentUrl':src,
        'previewImageUrl':srcPreview,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function pushButtons(to, thumbnailImageUrl, title, text, actions) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    'messages': [{
      "type" : 'template',
      "altText": "'buttons template'に非対応のため別の端末で確認してください。",
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": thumbnailImageUrl,
        "title": title,
        "text": text,
        "actions":actions,
      },
    }]
  };
  Logger.log(postData)

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function pushSticker(to, packageId, stickerId) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'sticker',
        'packageId':packageId,
        'stickerId': stickerId,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

function pushLocation(to, title, address, latitude, longitude) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'location',
        'title':title,
        'address':address,
        'latitude':latitude,
        'longitude':longitude,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}



/**
 * reply_tokenを使ってreplyする
 */
function reply(replyToken, text) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

function replySticker(replyToken, packageId, stickerId) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'sticker',
        'packageId':packageId,
        'stickerId': stickerId,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);   
}

function replyImage(replyToken, src) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'image',
        'originalContentUrl':src,
        'previewImageUrl':src,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);    
}

GASプロジェクトを公開する

LINEのwebhookからデータを遅れるように、作成したGASプロジェクトを公開してあげます。 GASの上のメニューの「公開」→「ウェブアプリケーションとして導入」を選択。 以下の項目は間違えやすいので気をつけましょう。 次のユーザとしてアプリケーションを実行:自分(****@gmail.com) アプリケーションにアクセスできるユーザ:全員(匿名ユーザを含む)

以上の設定で公開した時にURLが表示されるので、しっかりとコピペしてメモしておきましょう。

LINEのwebhookに登録する

今回はLINEのMessaging APIを使用します。 登録手順はここでは省きますが、作成したボットのwebhookに先程メモしたURLを貼っておきましょう。

実際に動かしてみる

テキトーに文字を送ってみて下さい。 今回は文字ならなんでもおっけーなプログラムになっています。

f:id:ponkotsu0605:20200426153637p:plain

もしも接続されていない場合はこのような表示がされます。 スピードクラスを選択することで、指定したクラスで通信をONにしてくれます。

f:id:ponkotsu0605:20200426153824p:plain

もし、ONの状態だった場合はこのような表示になります。 切断を選択すると切断することができます。

f:id:ponkotsu0605:20200426153850p:plain

また、クラスを変更することも可能です。

おまけ機能

せっかくなのでGASの定期実行を利用してみましょう。

自動OFF機能

このようにできても接続をOFFにし忘れたら意味がありません。 そこで、soracomAutoOffline()というメソッドを毎日呼ぶようにGASの設定をすることで、毎日OFFにすることができます。 自分は4時〜5時に実行するように設定しています。

f:id:ponkotsu0605:20200426154003p:plain

料金案内機能

毎日の料金が気になりますね! そこで、料金も送るように設定してみます。 soracomGetBillsLatest()を毎日送るように設定することで、使用している料金を毎日送ることができます。

f:id:ponkotsu0605:20200426154050p:plain

さいごに

このようにしてiPhone6sを節約しながら使っています。 通信速度も今のところは気になっていません。また、Wi-Fi環境ではなるべくWi-Fiを使うとより節約ができます。

格安SIMの選択肢にSORACOMを使ってみてはいかがでしょうか!

お問い合わせプライバシーポリシー制作物