Sigfox CallbackとGoogle Cloud Platform連携

Google Cloud PlatformとSigfoxとの連携方法を説明します。今回の例は、Sigfoxデバイスから送信されたメッセージをSigfoxクラウドのCallback機能で、GCPに接続。Cloud FunctionsからPub/Sub、Data FlowからBigQueryまでを対象とします。
※こちら[（動画）](https://youtu.be/t2liH7h69z4)[（チュートリアル）](https://cloud.google.com/community/tutorials/sigfox-gw)も参考に。
![GCP ZService (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/4d7d57b8-4866-2bda-2cf4-949e85de3fb4.png)

#事前準備
事前に、Google Cloud PlatformのアカウントとSigfoxバックエンドのアカウントを取得してください。Sigfoxバックエンドクラウドのアカウントをお持ちでない方は、[Sigfox Shield for Arduino](https://www.kccs-iot.jp/solution/product/device10/)や[M5Stack COM.X Sigfox](https://www.kccs-iot.jp/solution/product/devkit7/)をご購入いただき、[Sigfox Buy](https://buy.sigfox.com/)からアカウントを取得してください。

#Google Cloud Platformプロジェクトの作成
下記手順でGCPプロジェクトを作成してください

1. [GCPコンソール](https://console.cloud.google.com/?_ga=2.115825990.-1356959578.1547546490)に移動
2. 画面左上のプロジェクト選択後、【新しいプロジェクト】を選択
3. プロジェクト名を入力し【作成】ボタンをクリック（今回は”Sigfox-GCT"というプロジェクト名で説明します）
<img width="417" alt="gct01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/f18b5b5d-8490-8099-1870-00cbe07c8415.png">
4. 再度、プロジェクト選択をクリックし、先ほど作成した新規プロジェクトを選択
<img width="578" alt="gct02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/1c4f2fd7-f1af-c3b4-ff3b-60577aaaf4d1.png">
5. 左ナビゲーションメニューから"APIとサービス" > "ライブラリ"を開く
6. 下記APIが有効になっているかを確認し、もし有効になっていない場合は有効にしてください

- Cloud Functions API
- Cloud Pub/Sub API
- Cloud Datastore API
- Cloud Build API

#Sigfox Callback用Cloud Functionsをデプロイ
1.ローカル開発環境に[Google Cloud SDK](https://cloud.google.com/sdk/install) (gcloudコマンドラインツール)、git、python、pip、virtualenv、curlをインストールしてください。

2.先程作成したGCPプロジェクトの初期設定を下記コマンドで行います
`$ gcloud init`

3.Sigfoxデバイスのデータを受け取るPub/Sub topicを作成
`$ gcloud pubsub topics create sigfox-data`

4.必要に応じ、上記topicのメッセージをモニタリングするためのPub/Subサブスクリプションを作成します
`$ gcloud pubsub subscriptions create sigfox-data-sub --topic sigfox-data`

5.Sigfoxコールバックを受け付けるCloud Functions用に下記3ファイルを適当なフォルダ下に作成します。（ここでは"sigfox-gw"フォルダを作成し、そこに保存しておきます。）

```\main.py
import sys

def callback_data(request):
  # [START functions_callback_data]

  import os
  import json
  import base64
  import datetime
  from flask import abort
  from rfc3339 import rfc3339
  from google.cloud import datastore, pubsub_v1

  if request.method == 'POST':
    http_user = os.environ.get('HTTP_USER')
    http_passwd = os.environ.get('HTTP_PASSWD')
    request_user = request.authorization["username"]
    request_passwd = request.authorization["password"]

    if request_user == http_user and request_passwd == http_passwd:
      request_dict = request.get_json()
      print('Received Sigfox message: {}'.format(request_dict))

      publisher = pubsub_v1.PublisherClient()
      topic = os.environ.get('PUBSUB_TOPIC_DATA')
      project_id = os.environ.get('GCP_PROJECT')
      if not topic or not project_id:
          print('Error reading Function environment variables')
          return abort(500)
      topic_name = 'projects/{project_id}/topics/{topic}'.format(
          project_id=project_id, topic=topic)

      try:
          time_int = int(request_dict['time'])
      except ValueError:
          time_int = float(request_dict['time'])
          time_int = int(time_int)
      event_time = rfc3339(time_int)
      request_json = json.dumps(request_dict)
      message_id = publisher.publish(topic_name, request_json.encode('utf-8'),
                                     ts=event_time.encode('utf-8'))
      print('Message published to Pub/Sub topic: {}'.format(topic_name))
      return '', 200
    else:
      print('Invalid HTTP Basic Authentication: '
            '{}'.format(request.authorization))
      return abort(401)
  else:
      print('Invalid HTTP Method to invoke Cloud Function. '
            'Only POST supported')
      return abort(405)
  # [END functions_callback_data]
```

```\.env.yaml
PUBSUB_TOPIC_DATA: sigfox-data
HTTP_USER: user
HTTP_PASSWD: password
```
>.env.yamlファイルの`HTTP_USER`と`HTTP_PASSWORD`をご自由に設定してください。これらの項目は、SigfoxバックエンドクラウドからGCPにCallbackする際のCloud FunctionsがHTTPS通信時に、Sigfoxバックエンドを認証するために使用するBasic認証情報となります。

```\requirements.txt
rfc3339
google-cloud-pubsub
```

6.sigfox-gwディレクトリに移動します
`$ cd sigfox-gw`

7.新たにPythonの仮想環境を作成します
`$ py -3.7 -m venv myvenv` (windows)
`$ python3.7 -m venv myvenv` (mac, linux)

8.Pythone仮想環境を起動します
`$ myvenv\Scripts\activate` (windows)
`$ source myvenv/bin/activate` (mac, linux)

9.必要なPythonモジュール群をインストールします。
`(myvenv) $ pip install -r requirements.txt`

10.下記コマンドを使用し、Cloud Functionsをデプロイします。（--region値は、ご自身がデータの蓄積・処理を希望するCloud Functionsリージョンを設定してください。）
`(myvenv) $ gcloud functions deploy --region asia-northeast1 --runtime python37 --env-vars-file .env.yaml --trigger-http callback_data`

このコマンドを実行すると、下記例のようなHTTPS Trigger URLがコマンド出力されます。

```
httpsTrigger:
  url: https://asia-northeast1-sigfox-gcp.cloudfunctions.net/callback_data
```
このURLはCloud FunctionsのリージョンやGCPプロジェクト名に応じ、下記フォーマットで出力されます。
**https://[リージョン]-[プロジェクト名].cloudfunctions.net/callback_data**

11.デプロイが終了すれば、[Cloud FunctionsのWebコンソール](https://console.cloud.google.com/functions)で、Cloud Functionsがデプロイされていることを確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/e43e9465-a7d8-25d1-06dc-280441e08d82.png)

#Sigfox Callbackを作成
Sigfoxコールバックを受け付けるCloud Functionsが作成出来たので、次は、Sigfoxバックエンドクラウド上で、コールバックを作成します。
Sigfox Callbackは、デバイスタイプ単位で設定するため、[DEVICETYPE](https://backend.sigfox.com/devicetype/list)リストから、ご自身のデバイスのデバイスタイプを選択し、Callback設定を開始してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/563af0a4-0dba-03ff-031d-9b68f7085ac0.png)
AWSやAzureは専用のCallback作成メニューがありますが、GCPの場合は、**Custom callback**を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/863fdd22-0fee-24a0-acc8-27e334fb6af1.png)

各設定は下記の通りとなります。

| 項目 | 入力値 | 
|:-----------|:------------|
| Type | DATA_UPLINK |
| Channel | URL |
| Url pattern | 作成されたHTTPS Trigger URL* |
| Use HTTP Method | POST |
| Send SNI | ☑ |
| Headers | Authorization Basic ** |
| Content type | application/json |
| Body | { "deviceType" : "KCCS Network Monitoring", "device" : "{device}", "time" : {time}, "data" : "{data}", "seqNumber" : {seqNumber} } |

> ※GCP Cloud Functionsのデプロイで生成されたURL。（https://[リージョン]-[プロジェクト名].cloudfunctions.net/callback_data ）

> ※※Atuthorizationは、.env.yamlファイルで定義したHTTP_USERとHTTP_PASSWDから[Base64エンコード](https://www.blitter.se/utils/basic-authentication-header-generator/)した文字列となります。

全て設定し、[OK]ボタンをクリックするとCallbackが追加され、成功すると、下図のようにCloud Functionsのログで、SigfoxメッセージがPub/Sub Publishされていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/d0a0a8ce-12c3-1c8e-b3fc-f80d47ba61f9.png)

#Sigfoxメッセージ保存用データベースを作成
次にSigfoxメッセージを保存するデータベースを作成します。

##BigQueryテーブルの作成
1. Google Cloud Consoleの[BigQuery](https://console.cloud.google.com/bigquery)画面へ移動します。
2. 自身のプロジェクトにデータセットを作成します。データセットIDは任意（ここではds_sigfox）、ロケーションは今回は東京(asia-northeast1)にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/14d498de-29af-4ea3-58dd-fc3d44a86938.png)

3. 作成したデータセットを選択し、テーブルを新規に作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/656ea584-9376-7587-2fb2-2512c00c910e.png)
テーブルを作成する際には、Pub/Sub で入力されるデータ構造に準拠したテーブル定義をする必要があります。今回は、Sigfoxコールバックとして設定したdeviceType, device, time, data, seqNumberの5項目になります。

4. テーブルが作成出来たら、テーブルの詳細画面から**表ID**をメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/7f50901c-83be-1941-b1f0-7faff110b70f.png)
表IDは、**[プロジェクトID]:[データソースID].[テーブルID]**になります。

#Pub/SubからBigQueryに保存するCloud Functionsを作成
>Pub/SubからBigQueryにデータ保存する方法は、Dataflowを使うと簡単にできますが、Dataflowの料金体系が、数個のデバイスだけの場合には割高になるため、ここではCloud Functionsで実装します。

上述の[Sigfox Callback用Cloud Functionsをデプロイ](https://qiita.com/ghibi/items/bdccaf0bd0813bd82b3a#sigfox-callback%E7%94%A8cloud-functions%E3%82%92%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)と同様に下記コードをデプロイします。

```main.py
import os
import base64
import json
from google.cloud import bigquery

def main(event, context):
    """Triggered from a message on a Cloud Pub/Sub topic.
    Args:
         event (dict): Event payload.
         context (google.cloud.functions.Context): Metadata for the event.
    """
    PROJECT    = os.environ.get('PROJECT')
    DATASET    = os.environ.get('DATASET')
    TABLE      = os.environ.get('TABLE')
    print(DATASET)

    pubsub_message = base64.b64decode(event['data']).decode('utf-8')
    print(pubsub_message)

    d = json.loads(pubsub_message)

    output_rows = []

    output_rows.append([
        d['deviceType'],
        d['device'],
        d['time'],
        d['data'],
        d['seqNumber']
     ])

    bq_client   = bigquery.Client('zservice-295907')
    dataset_ref = bq_client.dataset('ds_pressure')
    table_ref   = dataset_ref.table('tb_pressure')
    table_obj   = bq_client.get_table(table_ref)
    error       = bq_client.insert_rows(table_obj, output_rows)

```

```requirement.txt
google-api-python-client
google-cloud-bigquery
```

```.env.yaml
PROJECT: [ご自身のプロジェクト名]
DATASET: [ご自身のプロジェクト名]:[BigQueryのデータセット名]
TABLE: [ご自身のプロジェクト名]:[BigQueryのデータセット名].[テーブル名]
```

上記3種類のコードを準備できれば、下記コマンドを使用し、Cloud Functionsをデプロイします。
`gcloud functions deploy FUNCTION_NAME --trigger-topic TOPIC_NAME FLAGS`
ここでは、FUNCTION_NAME = storeToBigQuery、TOPIC_NAMEは、先に作成したPub/Subトピックであるsigfox-dataを指定します。FLAGSは、デプロイ時に指定するオプションですので、ここでは、`--runtime python37 --env-vars-file .env.yaml` としておきます。

#動作確認
BigQueryのテーブルを確認すると、Sigfoxデバイスからのメッセージが保存されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/319ed8d0-be25-63cc-b30b-a25fd2373e4f.png)

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/d6ad10bc-75ba-8124-14d4-f144657c6473.png)](https://twitter.com/ghibi) 
[Sigfox Japan KCCS](https://qiita.com/organizations/sigfox)