sakura.ioのincoming-webhookを利用してデバイスにデータ送信

# 概要

外部からsakura.ioのデバイス（SCM-LTE-01）に向けてデータを送信します。色々と方法はありますが、ここでは、incoming-webhookを使用し、PC上のプログラム（Python/F#）からデバイスに向けてデータを送ってみたいと思います。

これを行なうためにやるべきことは、次の3つです。

- デバイス側で実行する受信プログラム（Python）を作成する
- sakura.ioプラットホームでincoming-webhookサービスを有効化する
- PC側で実行する送信プログラム（F#/Python）を作成する

# 受信プログラムの作成・実行

受信側は「SCM-LTE-01(sakura.io)」+「Raspi HAT(SCO-RPI-01)」+「RaspberryPi 3 model B+」という構成を想定します。プログラムは、SakuraIOSMBus というライブラリを使用してPython3.6で書いていきます。

基本的には、(1) 受信キュー内にデータが届いているかを`get_rx_queue_length`で調べて、(2) データがあれば`dequeue_rx_raw()`によりそれを取り出すという流れになります。なお、`dequeue_rx_raw()`の代わりに`peek_rx_raw()`を使うとキューに残したままデータを取得することができます。

```Python
import time
from sakuraio.hardware.rpi import SakuraIOSMBus
sakuraio = SakuraIOSMBus()

if sakuraio.get_is_online() :
  while True:
    if (sakuraio.get_rx_queue_length()).get('queued') :
      print(sakuraio.dequeue_rx_raw())
    time.sleep(1)
else :
  print(f'デバイスがLTE回線に接続できていません。')
```

# sakura.ioプラットホームの設定

PCからデバイスに対して、直接、データを送信することはできません。ここでは、sakura.ioプラットホームが提供する incoming-webhook というサービスを経由してPCからデバイスに向けてデータを送信する方法をとります。この方法では、incoming-webhook サービスにより指定されるURLに対して、デバイスに送りたいデータを含んだ HTTP POST リクエストメッセージを送れば、あとはsakura側でデバイスまでデータを届けてくれます。

## 連携サービスにincoming-webhookを追加
ブラウザからsakura.ioコントロールパネルにアクセスして、プロジェクトを選択して「連携サービスの追加」を行ないます。連携サービスとして次のように「incoming-webhook」を選択します。

![2019-02-09_11h21_26.png](https://qiita-image-store.s3.amazonaws.com/0/152805/5bf1236b-4669-9fe9-b6c0-ca204429a440.png)

## incoming-webhook の設定

名前とSecretを設定します。ここでは、名前を「Webhookでデバイスにデータ送信」、Secretを「1234」とします。

なお、Secretは、HTTPリクエストメッセージが改ざんされていないことを確認したり、誰かが勝手にデバイスに対してデータを送信することを防ぐために使う値です。運用時には別の文字列を使い、他人には知られないようにしてください。なお、Secretを空欄にすることもできます。

![2019-02-09_11h32_13.png](https://qiita-image-store.s3.amazonaws.com/0/152805/fb38911e-3cbc-142b-b225-a4cedf28712e.png)

## HTTPリクエストを送信する宛先URLの確認

連携サービスの一覧から、先ほど作成したincoming-webhookの設定にアクセスします。

![2019-02-09_11h29_52.png](https://qiita-image-store.s3.amazonaws.com/0/152805/00babcf6-fb01-a5fa-0e65-67f6a2ee4684.png)

**URL欄とToken欄を確認**しておきます。後ほど、このURLに対して所定のJSON形式でHTTP POSTリクエストメッセージを送信することになります。

![2019-02-09_11h35_542.png](https://qiita-image-store.s3.amazonaws.com/0/152805/454ac3cf-40d0-5f8e-8cf1-19884f7474f3.png)

## データ送信テスト

HTTPリクエストを送るプログラムを作成しなくとも、ブラウザを使って送信テストをすることができます。

Secretを設定したままではテストが面倒なので、ここでは、**一旦、Secretを空欄にして保存ボタンを押して**、Token欄のテキストをコピーしてから「Incoming Webhook APIドキュメント」をクリックしてください。

![2019-02-09_11h54_22.png](https://qiita-image-store.s3.amazonaws.com/0/152805/914a6198-53ac-bd20-3c87-0c483f3cf955.png)

移動したら、Token欄にコピーしておいたテキストをペーストします。また、module欄にモジュールIDを設定します。このモジュールIDは、sakura.ioコントロールパネルのモジュールカテゴリの以下の画面から確認できます。

![2019-02-09_12h01_29.png](https://qiita-image-store.s3.amazonaws.com/0/152805/93083af6-381b-d1b2-d49d-aea224f0942c.png)

その他、次のように設定します（チャンネル1に98という整数値を送信する例になっています）。

![2019-02-09_12h03_55.png](https://qiita-image-store.s3.amazonaws.com/0/152805/da77d475-73b1-c24b-ee42-c0af89a3c3a7.png)

設定ができたら、先に作成しておいた受信側のプログラムを起動してから「Try it out!」のボタンを押します。次のようにResponse Codeが200になっていれば成功で、デバイス側では、コンソールに`{'channel': 1, 'type': 'i', 'data': [98, 0, 0, 0, 0, 0, 0, 0], 'offset': 673}`のような出力がされていると思います。

![2019-02-09_12h14_01.png](https://qiita-image-store.s3.amazonaws.com/0/152805/321d11ba-fb61-a5d1-2524-fe3590a32452.png)

### うまくいかない？

Incoming WebhookにSecret「1234」を設定したままであったり、X-Sakura-SignatureにSecret値をそのまま設定している場合は、Response Codeが「401」で失敗します。X-Sakura-Signatureにすべき値（文字列）は、Secret と RequestBody を使ってHMAC-SHA1というハッシュ計算を行なった結果の文字列になります。

![2019-02-09_12h19_24.png](https://qiita-image-store.s3.amazonaws.com/0/152805/11e60001-7b9b-be8f-c567-0c471194fc24.png)

# 送信プログラムの作成

Incoming Webhook に対して、次のようなHTTP POSTリクエストメッセージを送信することができれば、Pythonでも、C#でも、JavaScriptでも、どんなプログラム言語でも送信プログラムを作成することができます。プログラムを作成しなくとも、コマンドの組合せで送信することもできます。

```
POST https://api.sakura.io/incoming/v1/xxxxxxxx-xxxxxxxxx-xxxx-xxxxxxxxxxxx HTTP/1.1
Accept: application/json
Content-Type: application/json
Content-Length: 104
Host: api.sakura.io

{"type":"channels","module":"uxxxxxxxxxxx","payload":{"channels":[{"channel":0,"type":"i","value":10}]}}
```

以降は、sakuraコントロールパネルで、**Secretを「1234」に戻した**という想定でのプログラムになります。

## 送信プログラム Python3.6版

```Python
import hmac
import hashlib
import http
import json
import requests

def genHMACSHA1(key,msg):
    key = key.encode()
    msg = msg.encode()
    return hmac.new(key,msg,hashlib.sha1).hexdigest()

Token = 'xxxxxxxx-xxxxxxxxx-xxxx-xxxxxxxxxxxx' # 要変更
ModuleID = 'uxxxxxxxxxxx'                      # 要変更
Secret = '1234'                                # 要変更

channelData = list()
channelData.append( {'channel':0,'type':'i','value':10} ) # 要変更
channelData.append( {'channel':1,'type':'i','value':20} ) # 要変更

postBody = dict()
postBody['type'] = 'channels'
postBody['module'] = ModuleID
postBody['payload'] = {'channels':channelData}

jsonString = json.dumps(postBody)
#print(jsonString)

headers = dict()
headers['Accept']='application/json'
headers['Content-Type']='application/json'
headers['X-Sakura-Signature'] = genHMACSHA1(Secret,jsonString)

ApiURL = f'https://api.sakura.io/incoming/v1/{Token}'
resMsg = requests.post(ApiURL, jsonString, headers=headers)

resMsgStatusLine= f'{resMsg.status_code} '
resMsgStatusLine+= f'{http.client.responses[resMsg.status_code]} HTTP/'
resMsgStatusLine+= '.'.join(list(str(resMsg.raw.version)))

resMsgHeader = '\n'.join(f'{k}: {v}' for k, v in resMsg.headers.items())+'\n'
resMsgBody = resMsg.text;

print(resMsgStatusLine)
print(resMsgHeader)
print(resMsgBody)
```

### 実行結果

```
200 OK HTTP/1.1
Server: nginx
Date: Sat, 09 Feb 2019 04:36:43 GMT
Content-Type: application/json
Content-Length: 15
Connection: keep-alive

{"status":"ok"}
```

## 送信プログラム F#版（RestSharp利用）


```FSharp
open System
open System.Text
open System.Security
open RestSharp
open Newtonsoft.Json
open System.Runtime.Serialization
open Microsoft.FSharp.Collections

[<DataContract>]
type tChannelData = {
  [<DataMember>]  channel:int
  [<DataMember(Name="type")>] type_:string
  [<DataMember>]  value:int
}

[<DataContract>]
type tPayload = {
  [<DataMember>]  channels:List<tChannelData>
}

[<DataContract>]
type tPostData = {
  [<DataMember(Name="type")>]   type_:string
  [<DataMember(Name="module")>] module_:string
  [<DataMember>]  payload:tPayload
}

let genHMACSHA1 (key:string) (msg:string) =
  let bKey = Encoding.UTF8.GetBytes(key);
  let bMsg = Encoding.UTF8.GetBytes(msg)
  let hmac = new Cryptography.HMACSHA1(bKey);
  let bs = hmac.ComputeHash(bMsg);
  do hmac.Clear()
  BitConverter.ToString(bs).ToLower().Replace("-", "");

[<EntryPoint>]
let main argv =

  let token = "xxxxxxxx-xxxxxxxxx-xxxx-xxxxxxxxxxxx" // 要変更
  let moduleID = "uxxxxxxxxxxx"                      // 要変更
  let secret   = "1234";                             // 要変更

  let postBody = {
    type_ = "channels"
    module_ = moduleID
    payload = {
      channels = [ { channel=0; type_="i"; value=10 }   // 要変更
                   { channel=1; type_="i"; value=20 } ] // 要変更
    }
  }

  let jsonString = JsonConvert.SerializeObject(postBody) 
  let signature = jsonString |> genHMACSHA1 secret
  //printfn "%s" jsonString

  let request = new RestRequest( Method.POST)
  request.AddHeader("Accept", "application/json") |> ignore
  request.AddHeader("X-Sakura-Signature",signature) |> ignore
  request.AddParameter("application/json", jsonString, ParameterType.RequestBody) |> ignore

  let ApiURL = "https://api.sakura.io/incoming/v1/" + token
  let client = new RestClient(ApiURL)
  let res = client.Execute(request)
  
  printfn "%d %s" (int res.StatusCode) res.StatusDescription
  let nl = System.Environment.NewLine
  let rec headerText (headers:List<Parameter>) = 
    match headers with
    | h::t -> h.Name+":"+(string h.Value)+nl+(headerText t)
    | [] -> String.Empty
  printfn "%s" (List.ofSeq(res.Headers) |> headerText )
  printfn "%s" (res.Content)
  Console.ReadKey() |> ignore
  0
```

### 実行結果

```
200 OK
Server:nginx
Date:Sat, 09 Feb 2019 05:36:11 GMT
Connection:keep-alive
Content-Type:application/json
Content-Length:15

{"status":"ok"}
```


## 送信プログラムF#版（FSharp.Data利用）

- 2019/02/19追記
- FSharp.Dataを利用したほうがすっきりと記述できます。



```FSharp
open System
open System.Text
open System.Security
open Newtonsoft.Json
open System.Runtime.Serialization
open Microsoft.FSharp.Collections
open FSharp.Data
open FSharp.Data.HttpRequestHeaders

[<DataContract>]
type tChannelData = {
  [<DataMember>]  channel:int
  [<DataMember(Name="type")>] type_:string
  [<DataMember>]  value:int
}

[<DataContract>]
type tPayload = {
  [<DataMember>]  channels:List<tChannelData>
}

[<DataContract>]
type tPostData = {
  [<DataMember(Name="type")>]   type_:string
  [<DataMember(Name="module")>] module_:string
  [<DataMember>]  payload:tPayload
}

let genHMACSHA1 (key:string) (msg:string) =
  let bKey = Encoding.UTF8.GetBytes(key);
  let bMsg = Encoding.UTF8.GetBytes(msg)
  let hmac = new Cryptography.HMACSHA1(bKey);
  let bs = hmac.ComputeHash(bMsg);
  do hmac.Clear()
  BitConverter.ToString(bs).ToLower().Replace("-", "");

[<EntryPoint>]
let main argv =

  let token = "xxxxxxxx-xxxxxxxxx-xxxx-xxxxxxxxxxxx" // 要変更
  let moduleID = "uxxxxxxxxxxx"                      // 要変更
  let secret   = "1234";                             // 要変更

  let postBody = {
    type_ = "channels"
    module_ = moduleID
    payload = {
      channels = [ { channel=0; type_="i"; value=21 }
                   { channel=1; type_="i"; value=37 } ]
    }
  }

  let jsonString = JsonConvert.SerializeObject(postBody) 
  let signature = jsonString |> genHMACSHA1 secret

  let ApiURL = "https://api.sakura.io/incoming/v1/" + token
  
  async {
    let! res = Http.AsyncRequest( ApiURL, 
                                  headers = [ ContentType HttpContentTypes.Json
                                              Accept HttpContentTypes.Json
                                              ("X-Sakura-Signature",signature)] , 
                                  body = TextRequest jsonString )
    printfn "%d" res.StatusCode
    for (KeyValue(key,value)) in res.Headers do 
      printfn "%s:%s" key value 

  } |> Async.Start

  Console.ReadKey() |> ignore
  0
```