SORACOM、わくわくしますね。しませんか? え、「SORACOMって何」ですって? そういう方は、クラウド業界の住人としては少しアンテナが鈍っているのかもしれません。先日開発者向けイベント「SORACOM Developers Conference #0」に参加してきたのですが、抽選で200名の会場を埋め尽くしUstream配信も行うなど、開発者を中心に期待が高まっているサービスです。
SORACOMを簡単に一言で言うと、「プログラマブルな」「割安の」SIMカードを提供するMVNOサービスです。しかしながら、カスタマイズ性そのもの、あるいは現在の価格そのものに目を向けるというよりは、こちらの記事辺りをご覧になりながら、その将来性を判断するべきサービスと考えられます。
これまでSIMカードからの通信内容をプログラムから操作することは不可能だったので、まずは試してみたい、という人もいるのでしょう。1000円弱で購入でき、2年縛りがなくいつでも終了できるところも個人が扱いやすいと言えます。AWS登場時、特に個人がサーバーをちょっと借りる目的として注目されたことを思い出させます。
しかし、それのみならず、IoT利用での利用に適するようサービス設計されていることを含め、企業がこのプラットフォームを使うメリットも大きいものです。例えばこれらの記事を読むと、SORACOMの実用面でのメリットを感じることができることでしょう。
アピリオはB2Bのシステムコンサルティング会社ですので、この記事でも企業での利用ケースについて検討してみたいと思います。
今回は、外出の多い社員向けの携帯端末にSORACOMのSIMカードを提供し、営業時間中のみ通信可能なSIMカードとして運用することを想定してみます。SORACOMにも独自の管理画面がありますが、端末情報は企業システムであるSalesforce上で管理することにし、SalesforceからSORACOMのAPIを実行するサンプルを作ることにします。
まずは、管理用のオブジェクトを作成します。SIMカード1枚ごとに1レコード作成するイメージです。IDに相当するIMSI番号を入れておきましょう。また、端末ごとに利用可能時間を設定できるようにしています。
続いて、SORACOM APIアクセス用のApexクラスを用意します。今回必要となった部分以外は作っていないのですが、素直なAPIなので順次追加して行けるものと思います。リモートサイトの設定画面にてSORACOMのAPIエンドポイントであるhttps://api.soracom.ioを設定するのもお忘れなく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
/** * soracom.jpへのApexインターフェース */ public class SoracomClient { public static String API_BASE_URL = 'https://api.soracom.io/v1'; public static String PATH_AUTH = '/auth'; public static String PATH_SUBSCRIBER_LIST = '/subscribers'; public static String PATH_SUBSCRIBER_REGISTER = '/subscribers/{imsi}/register'; public static String PATH_SUBSCRIBER_ACTIVATE = '/subscribers/{imsi}/activate'; public static String PATH_SUBSCRIBER_DEACTIVATE = '/subscribers/{imsi}/deactivate'; public integer requestTimeout = 60000; public String apiKey; public String token; public String operatorId; public HttpRequest lastHttpRequest; public HttpResponse lastHttpResponse; /** * SORACOMのコンソールに入る時に使用するのと同じemailおよびパスワード */ public SoracomClient(String email, String password) { auth(email, password); } public Boolean isLastRequestSucceeded() { HttpResponse res = this.lastHttpResponse; return res == null || res.getStatusCode() < 400; } public String getLastResponseMessage() { HttpResponse res = this.lastHttpResponse; if (res == null) return null; Map<String, Object> responseMap =(Map<String, Object>)JSON.deserializeUntyped(res.getBody()); return (String)responseMap.get('message'); } public HttpRequest getCommonHttpRequest() { return getCommonHttpRequest(true); } /** * @param includesToken /auth APIのようにX-Soracom-API-KeyおよびX-Soracom-Tokenが不要な時はfalseを渡す */ public HttpRequest getCommonHttpRequest(Boolean includesToken) { HttpRequest req = new HttpRequest(); req.setHeader('Content-Type', 'application/json'); req.setHeader('Accept', 'application/json'); req.setTimeout(requestTimeout); if (includesToken) { req.setHeader('X-Soracom-API-Key', apiKey); req.setHeader('X-Soracom-Token', token); } return req; } /** * 認証し、Operator IDを返す。失敗したらnullを返す */ public String auth(String email, String password) { Map<String, Object> requestParameterMap = new Map<String, Object>(); requestParameterMap.put('email', email); requestParameterMap.put('password', password); String requestParameter = JSON.serialize(requestParameterMap); HttpRequest req = getCommonHttpRequest(false); req.setMethod('POST'); req.setEndpoint(API_BASE_URL + PATH_AUTH); req.setBody(requestParameter); Http http = new Http(); HttpResponse res = http.send(req); this.lastHttpRequest = req; this.lastHttpResponse = res; if (res.getStatusCode() != 200) return null; Map<String, Object> responseMap =(Map<String, Object>)JSON.deserializeUntyped(res.getBody()); this.apiKey = (String)responseMap.get('apiKey'); this.operatorId = (String)responseMap.get('operatorId'); this.token = (String)responseMap.get('token'); return this.operatorId; } /** * Subscriber一覧を返す * リクエストに失敗した時にも空のリストを返す * imsi, statusなど */ public List<Map<String, Object>> listSubscribers() { HttpRequest req = getCommonHttpRequest(); req.setMethod('GET'); req.setEndpoint(API_BASE_URL + PATH_SUBSCRIBER_LIST); Http http = new Http(); HttpResponse res = http.send(req); this.lastHttpRequest = req; this.lastHttpResponse = res; List<Map<String, Object>> subscribers = new List<Map<String, Object>>(); if (res.getStatusCode() >= 400) return subscribers; List<Object> responseList =(List<Object>)JSON.deserializeUntyped(res.getBody()); for (Object response : responseList) { Map<String, Object> subscriberMap = (Map<String, Object>)response; subscribers.add(subscriberMap); } return subscribers; } /** * 指定したIMSIの登録 */ public Boolean registerSubscriber(String imsi, String registrationSecret) { Map<String, Object> requestParameterMap = new Map<String, Object>(); requestParameterMap.put('registrationSecret', registrationSecret); String requestParameter = JSON.serialize(requestParameterMap); System.debug(requestParameter); HttpRequest req = getCommonHttpRequest(); req.setMethod('POST'); String path = PATH_SUBSCRIBER_REGISTER.replace('{imsi}', imsi); req.setEndpoint(API_BASE_URL + path); req.setBody(requestParameter); Http http = new Http(); HttpResponse res = http.send(req); this.lastHttpRequest = req; this.lastHttpResponse = res; if (res.getStatusCode() >= 400) return false; return true; } /** * 指定したIMSIの有効化。成功すればtrue。 */ public Boolean activateSubscriber(String imsi) { HttpRequest req = getCommonHttpRequest(); req.setHeader('Content-Type', 'text/plain'); // overwrite req.setMethod('POST'); String path = PATH_SUBSCRIBER_ACTIVATE.replace('{imsi}', imsi); req.setEndpoint(API_BASE_URL + path); Http http = new Http(); HttpResponse res = http.send(req); this.lastHttpRequest = req; this.lastHttpResponse = res; if (res.getStatusCode() >= 400) return false; return true; } /** * 指定したIMSIの無効化。成功すればtrue。 */ public Boolean deactivateSubscriber(String imsi) { HttpRequest req = getCommonHttpRequest(); req.setHeader('Content-Type', 'text/plain'); // overwrite req.setMethod('POST'); String path = PATH_SUBSCRIBER_DEACTIVATE.replace('{imsi}', imsi); req.setEndpoint(API_BASE_URL + path); Http http = new Http(); HttpResponse res = http.send(req); this.lastHttpRequest = req; this.lastHttpResponse = res; if (res.getStatusCode() >= 400) return false; return true; } } |
では、実行してみましょう。SORACOMに登録したメールアドレス、パスワードと、SIMに記載されているIMSIおよびPASSCODEと書かれた登録キーを用意し、下記のようなコードにてSIMカードの登録が可能です(記述に誤りがあった場合は、"Invalid username/password supplied"、"No such resource found"といったメッセージが戻ってきます)。
1 2 3 4 5 6 7 8 9 10 |
SoracomClient client = new SoracomClient(your_soracom_email, your_soracom_password); if (!client.isLastRequestSucceeded()) { System.debug(client.getLastResponseMessage()); } else { Boolean succeeded = client.registerSubscriber(imsi, registration_key); if (!succeeded) System.debug(client.getLastResponseMessage()); else System.debug('register succeeded. imsi=' + imsi); } |
同様に、登録済のSIMカード(Subscriber)の一覧取得や、Subscriberの有効化無効化も簡単なAPI呼び出しにて実施できます。有効無効の設定が数秒で端末側に反映されるのが驚くべきことです。
1 2 3 |
List<Map<String, Object>> subscribers = client.listSubscribers(); Boolean succeeded = client.activateSubscriber(imsi); Boolean succeeded = client.deactivateSubscriber(imsi); |
このAPIアクセス用のクラス(SoracomClient
)は先ほど作成したオブジェクト(soracom_devices
)とは独立していますが、ここから実際に、レコードの使用開始時刻、使用終了時刻の項目値に合わせてAPIを呼び出し、その結果に合わせてレコードを更新する処理を行っていきます。ただし、現在のところSORACOM APIでは使用可能時間帯を直接設定することはできません。そのため、Salesforceで毎時スケジュールジョブを実行し、各レコードの設定値に応じて随時ステータス有効化、無効化のAPIを呼び出すことにて要件を実現します。
今回はSoracomClient
クラスを呼び出すSchedulable
なクラス(SoracomSchedulableOperation
)を作成しました。テストの都合上似たようなメソッドが幾つかありますが、スケジュール登録して実行するためには、@future(callout=true)
のstatic void
メソッドを経由して処理を行う必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
public class SoracomSchedulableOperation implements Schedulable, Database.Stateful, Database.AllowsCallouts { public String email; public String password; public SoracomSchedulableOperation(String email, String password) { this.email = email; this.password = password; } public void execute(SchedulableContext sc) { SoracomSchedulableOperation.executeFutureCallout(email, password); } /** * スケジュール登録して実行する時はcallout設定が必須 */ @future(callout=true) public static void executeFutureCallout(String email, String password) { doExecuteFutureCallout(email, password); } public static void doExecuteFutureCallout(String email, String password) { SoracomClient client = new SoracomClient(email, password); if (!client.isLastRequestSucceeded()) { System.debug(LoggingLevel.ERROR, 'SoracomScheduledOperation Authentication Failed' + ' (' + client.getLastResponseMessage() + ').'); return; } soracom_devices__c[] succeededRecords = new soracom_devices__c[]{}; soracom_devices__c[] failedRecords = new soracom_devices__c[]{}; Integer currentHour = Datetime.now().hour(); for (soracom_devices__c record : [SELECT Name, IMSI__c, status__c FROM soracom_devices__c WHERE starthour__c <= :currentHour AND endhour__c > :currentHour AND IMSI__c != null]) { String imsi = record.IMSI__c; Boolean succeeded = client.activateSubscriber(imsi); if (!succeeded) { System.debug(LoggingLevel.ERROR, 'SoracomScheduledOperation Activate Failed for ' + imsi + ' (' + client.getLastResponseMessage() + ').'); failedRecords.add(record); continue; } record.status__c = 'activate'; succeededRecords.add(record); } for (soracom_devices__c record : [SELECT Name, IMSI__c, status__c FROM soracom_devices__c WHERE (starthour__c > :currentHour OR endhour__c <= :currentHour) AND IMSI__c != null]) { String imsi = record.IMSI__c; Boolean succeeded = client.deactivateSubscriber(imsi); if (!succeeded) { System.debug(LoggingLevel.ERROR, 'SoracomScheduledOperation Deactivate Failed for ' + imsi + ' (' + client.getLastResponseMessage() + ').'); failedRecords.add(record); continue; } record.status__c = 'inactivate'; succeededRecords.add(record); } update succeededRecords; System.debug(LoggingLevel.INFO, 'SoracomScheduledOperation Operation succeeded=' + succeededRecords.size() + ', failed=' + failedRecords.size() + '.'); } } |
毎時実行したいので、画面からではなくApexを実行してスケジュール登録します。
1 |
System.schedule('SoracomSchedulableOperation', '0 0 * * * ?', new SoracomSchedulableOperation(email, password)); |
実行されると、レコードの開始時刻、終了時刻設定に合わせて、SORACOMのユーザーコンソールでも設定が更新されていることが確認できます。また、API呼び出しに成功したレコードについて、Salesforce側のレコードも更新されています。
今回のサンプルはここまでですが、いかがでしたでしょうか。企業システムとSIMの連携のイメージは広がってきましたか? 例えば今回の延長で次のようなこともできそうです。
- 夜間に多くのデータを送信するIoTデバイスのために、通信速度を昼間と夜間で変更する
- 通信データ量を取得し各デバイスの通信量を一覧表示したり、通信量に応じて通信速度を変更したりする
- 各デバイス管理者にデバイスの使用状況を定期的にメールする
- SORACOM Beam経由でデバイスからSalesforceに直接安全にデータを登録する
なお、今回のサンプルで利用したスケジュールクラスでは、API呼び出しを直列に端末数の分だけ実行していますが、ご存知の通りcallout / futureにはガバナ制限やタイムアウトがありますので、端末数が増えた際は工夫が必要となります。併せて、グループやタグで指定した複数端末について一括操作を行うAPIの追加にも期待したいところです。