Azure IoT Hub - ジョブ（Python）

# 目的

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/6828538d-32a5-11b3-10ea-267b1c32f939.png)
上記のように[Microsoftドキュメント](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-python-python-schedule-jobs)にAzure IoT SDK for Pythonではジョブ機能がサポートされていないと記載があるが、[IoTHubJobManager Class](https://docs.microsoft.com/en-us/python/api/azure-iot-hub/azure.iot.hub.iothub_job_manager.iothubjobmanager?view=azure-python)が用意されているようなので実装する
Azure Functions（トリガー：EventGrid）上でダイレクトメソッドを実行するジョブを実装する

# 実装

## デバイス側

[ドキュメント](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-python-python-schedule-jobs#create-a-simulated-device-app)通りに実装
クラウドからコールされるダイレクトメソッドの登録を行う

## クラウド側

接続文字列の取得は[ドキュメント](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-python-python-schedule-jobs#get-the-iot-hub-connection-string)を参照
デバイスのダイレクトメソッドをコールするジョブを発行、ジョブ状態の監視を行う
以下実装

```python:__init__.py
import sys
import uuid
import json
import logging
import datetime
import time

import azure.functions as func

from azure.iot.hub import IoTHubJobManager
from azure.iot.hub.models import CloudToDeviceMethod, CloudToDeviceMethodResult, JobRequest

CONNECTION_STRING = ''
METHOD_NAME = 'lockDoor'
METHOD_PAYLOAD = '{\'lockTime\':\'10m\'}'

def executeJob():
  try:
    deviceMethod = CloudToDeviceMethod(method_name=METHOD_NAME, payload=METHOD_PAYLOAD)
    jobId = str(uuid.uuid4())
    logging.info('job ID: %s', jobId)

    jobRequest = JobRequest(job_id = jobId, 
      type = 'scheduleDeviceMethod', cloud_to_device_method = deviceMethod, 
      query_condition = 'deviceId IN [\'myDeviceId\']',
      start_time = datetime.datetime.now(datetime.timezone.utc), max_execution_time_in_seconds = 30)
    
    # 接続文字列からジョブマネージャーインスタンスを生成
    jobManager = IoTHubJobManager(CONNECTION_STRING)
    # ジョブ生成
    jobManager.create_scheduled_job(jobId, jobRequest)

    # ジョブ状態の監視
    while True:
      # ジョブ状態の取得
      response = jobManager.get_scheduled_job(jobId)
      logging.info('job status: %s', response.status)
      if response.status != 'running':
        break
      time.sleep(5)

  except Exception as ex:
    logging.info('')
    logging.info('Unexpected error {0}'.format(ex))
    return
  except KeyboardInterrupt:
    logging.info('')
    logging.info('IoTHubDeviceMethod sample stopped')

def main(event: func.EventGridEvent):

  result = json.dumps({
      'id': event.id,
      'data': event.get_json(),
      'topic': event.topic,
      'subject': event.subject,
      'event_type': event.event_type,
  })
  logging.info('Python EventGrid trigger processed an event: %s', result)

  # ジョブ実行
  executeJob()

```

以下ジョブのリクエスト、レスポンスで使用するクラス

- [JobRequest Class](https://docs.microsoft.com/en-us/python/api/azure-iot-hub/azure.iot.hub.protocol.models.jobrequest?view=azure-python)
- [JobResponse Class](https://docs.microsoft.com/en-us/python/api/azure-iot-hub/azure.iot.hub.protocol.models.jobresponse?view=azure-python)

# ジョブに関する制限事項

- ジョブの最大数は 1 (Free および S1)、5 (S2)、10 (S3) （[ドキュメント](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-devguide-quotas-throttling#other-limits)）
- Azure portal上でジョブの確認、操作ができない

## 懸念

[cancel_scheduled_job](https://docs.microsoft.com/en-us/python/api/azure-iot-hub/azure.iot.hub.iothub_job_manager.iothubjobmanager?view=azure-python#cancel-scheduled-job-job-id-)の仕様からジョブ発行後ジョブIDを忘れてしまった場合、ジョブのキャンセルができない、Azure portalからジョブのキャンセルも不可
アカウントがFree および S1の場合はジョブ最大数は1のため、それ以上ジョブの発行ができなくなる
最悪タイムアウトでジョブが終了するように`JobRequest`の`max_execution_time_in_seconds`は必ず設定しておいたほうが良いと思われる