セサミ スマートロックを操作するFIWARE Context Provider_sesami (ver.0.9)を作成しました

セサミ スマートロックをFIWARE環境(NGSI)で操作可能にするコンテキストプロバイダのコードを作成しました。
「FIWAREでスマートホームする」 シリーズの第一弾です！

## できること
FIWARE環境でセサミ　スマートロックのドア開閉状態を取得できます。また、操作もできます。NGSIで制御できるので、他のデータやシステムと組み合わせてドアを開閉できます。

(ただし残念ながら、セサミ側のAPI仕様により、ドア状態の変化をトリガーとして即時アクションを起こすことは困難です。)

いろいろなサービスのAPIをこのようにNGSI化しておけば、NGSIで色々組み合わせて動作させられるようになります。

## 使い方
### 前提
- FIWARE環境がある(Context Brokerが動いている)
- セサミ　スマートロックが設置・稼働している
- セサミのサイトでAPI KEYを取得している
　-　[セサミ Web API](https://doc.candyhouse.co/ja/SesameAPI "SESAMI Web API")を参考に、操作対象のスマートロックのUUID、SECRET-KEY, API-KEYを取得してください
- コードの実行に、pycryptodomeパッケージが必要です　

### インストール・設定のしかた
FIWARE環境がorion.example.comで稼働しているとします。

1. orion.exmaple.com上で、context_provider_sesami.pyを適当なディレクトリに配置します。
1. pycryptdomeパッケージをインストールします
``` $ pip3 install pycryptdome ```
1. 環境変数をセットします
``` $ export ORION_URL=https://orion.example.com```
``` $ export CONTEXT_PROVIDER_URL=http://orion.example.com:8081```
``` $ export SESAME_DOOR_UUID=xxxxxxx (取得したUUID)```
``` $ export SESAME_KEY_SECRET=yyyyyy　(取得したSECRET KEY)```
``` $ export SESAME_API_KEY=zzzzzz (取得した　API　KEY) ```
1. コンテキストプロバイダのコードを実行させます
``` $python3 ./context_provider_sesami.py```

### FIWARE環境での使い方
#### context providerとしての登録

``` bash:create-registration.sh
#!/bin/bash
: ${ORION_URL:?Not found}
: ${CONTEXT_PROVIDER_URL:?Not found}
: ${SESAME_DOOR_UUID:?Not found}
: ${SESAME_API_KEY:?Not found}
curl -iX POST \
  "$ORION_URL/v2/registrations" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer aaaaaaaaaaa(FIWARE BigBang環境の場合は、tokenをここにセット)" \
  -d "{
  \"description\": \"Door Context Source\",
  \"dataProvided\": {
    \"entities\": [
      {
        \"id\": \"urn:ngsi-ld:Door:door001\",
        \"type\": \"Door\"
      }
    ],
    \"attrs\": [
      \"openState\"
    ]
  },
  \"provider\": {
    \"http\": {
      \"url\": \"$CONTEXT_PROVIDER_URL/v1/Door\"
    }
  }
}"
```
``` $ bash ./create-registration.sh ```

このスクリプトを実行することで、ID=urn:ngsi-ld:Door:door001でcontext-providerが登録されます。

#### 値の取得

ngsiコマンドを使って、スマートドアの状態を取得する例
```sh:
$ ngsi get entity --id urn:ngsi-ld:Door:door001
{"id":"urn:ngsi-ld:Door:door001","type":"Door","openState":{"type":"string","value":"closed","metadata":{}}}
```
#### 鍵を開ける例
```
$ curl -X GET http://orion.example.com:8081/v1/Door/DoorOpen
```
(ここはいずれNGSIに準拠するように直したいと思っています。)
(これは、subscriptionで利用することを想定しています。)

## コード
（近いうちにGitHub上にも登録します。）
ver.0.9です。NGSI updateでドア状態を変更できるようにしたらVer.1.0にしたいと考えています。

本コード開発にあたっては、 [Let's FIWARE](https://www.letsfiware.jp/dev/ "Let's FIWARE") の[コンテキスト・プロバイダ型のFIWARE対応IoTデバイス(Raspberry Pi)](https://www.letsfiware.jp/dev/fiware-raspberry-pi-context-provider/ "コンテキスト・プロバイダ型のFIWARE対応IoTデバイス(Raspberry Pi)")　で紹介されているコンテキストプロバイダのコードを参考にさせていただいております。

```python:context_provider_sesami.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import signal
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import json
import urllib.request
import datetime, base64
from Crypto.Hash import CMAC
from Crypto.Cipher import AES

door_uuid = os.environ.get('SESAME_DOOR_UUID')
api_key = os.environ.get('SESAME_API_KEY')
key_secret = os.environ.get('SESAME_KEY_SECRET')

def get_door_state():
  url = 'https://app.candyhouse.co/api/sesame2/' + door_uuid
  headers = {'x-api-key': api_key}
  req = urllib.request.Request(url, None, headers)
  with urllib.request.urlopen(req) as res:
    data = res.read()

  if (debug):
    print(data)

  body = json.loads(data.decode('utf-8'))
  if body['CHSesame2Status'] == 'unlocked':
    rc = 'open'
  elif body['CHSesame2Status'] == 'locked':
    rc = 'closed'
  return(rc)

def change_door_state(command):
  url = 'https://app.candyhouse.co/api/sesame2/' + door_uuid + '/cmd'
  headers = {'x-api-key': api_key}

  history = 'test'
  base64_history = base64.b64encode(bytes(history, 'utf-8')).decode()
  ts = int(datetime.datetime.now().timestamp())
  message = ts.to_bytes(4, byteorder='little')
  message = message.hex()[2:8]
  cmac = CMAC.new(bytes.fromhex(key_secret), ciphermod=AES)
  cmac.update(bytes.fromhex(message))
  sign = cmac.hexdigest()

  if (command == 'open') or (command == 'unlocked'):
    cmd = 83
  elif (command == 'lock') or (command == 'closed'):
    cmd = 82
  elif command == 'toggle':
    cmd = 88

  body = {
    'cmd' : cmd,
    'history' : base64_history,
    'sign' : sign
  }
  req = urllib.request.Request(url, json.dumps(body).encode(), headers)
  with urllib.request.urlopen(req) as res:
    data = res.read()

def handler(signum, frame):
  sys.exit(0)

class ContextProviderHandler(BaseHTTPRequestHandler):
  def do_GET(self):
    path = self.path
    if (path != '/v1/Door/DoorOpen'):
      self.send_response(404)
      return

    change_door_state('open')

    self.send_response(200)

  def do_POST(self):
    content_length = int(self.headers['content-length'])
    path = self.path
    if (path != '/v1/Door/op/query'):
      self.send_response(404)
      return

    body = json.loads(self.rfile.read(content_length).decode('utf-8'))
    e = body['entities'][0]['id']
    if (e != device_id):
      self.send_response(404)
      return
      
    if (debug):
      print('path = {}'.format(path))
      print('body = {}'.format(body))

    response = [
      {
        "id": device_id,
        "type": "Door",
        'openState': {
          'type': 'string',
          'value': get_door_state()
        }
      }
    ]

    if (debug):
      print(response)

    self.send_response(200)
    self.send_header('Content-Type', 'application/json; charset=utf-8')
    self.end_headers()
    self.wfile.write(json.dumps(response).encode('utf-8'))
        
if __name__ == '__main__':
  signal.signal(signal.SIGTERM, handler)
  signal.signal(signal.SIGINT, handler)

  device_id = 'urn:ngsi-ld:Door:' + os.environ.get('DEVCIE_ID', 'door001')
  debug = not not os.environ.get('DEBUG_FLAG', '')
  debug = True

  print('Start Context Provider')
  address = ('0.0.0.0', 8081)
  with HTTPServer(address, ContextProviderHandler) as server:
      server.serve_forever()

```

## 今後
他のトリガーと組み合わせてドアを開閉するサンプルを作りたいと思っています。
使い方アイデアやコード改良案など、よろしければコメント等お願いします。

## 謝辞
本記事は全般に、[Let's FIWARE](https://www.letsfiware.jp/dev/ "Let's FIWARE") の[コンテキスト・プロバイダ型のFIWARE対応IoTデバイス(Raspberry Pi)](https://www.letsfiware.jp/dev/fiware-raspberry-pi-context-provider/ "コンテキスト・プロバイダ型のFIWARE対応IoTデバイス(Raspberry Pi)")　を参考にしています。Sesamiスマートロック用に書き換えたことが本記事の主要な貢献ということになります。ありがとうございます。

## 参考文献
FIWAREのコンテキスト・プロバイダについてはこちらが参考になります。

https://fiware-tutorials.letsfiware.jp/Context-Providers/

FIWARE(orion)のコンテキスト・プロバイダ実装についての詳細はこちら。

https://fiware-orion.letsfiware.jp/user/context_providers/

コンテキストプロバイダの実例はこちら。

https://www.letsfiware.jp/dev/fiware-raspberry-pi-context-provider/

SESAMIのAPIについてはこちら。

https://doc.candyhouse.co/ja/SesameAPI