スマートホームデバイスとして操作できるようにする

# １．はじめに
　前回の[スマートホームデバイス的な何かを作る](https://qiita.com/SSaitou/items/ec0b3a252934a371a82e )でGoogle Assistantに疑似デバイス（疑似アウトレット）を登録することができましたが、操作はできません。正確には、アプリ側で疑似アウトレットのオン・オフ操作はできるのですが、サーバー側ではそのステータスを持っていません。そもそもデバイス操作を扱うEXECUTEインテンツを扱っていないので取り扱えるようにロジックを追加する必要があります。
　また、ＡＰＩはGoogleから呼ばれた時に動くだけで、サービスとして常駐しない作りを今はしているので、ステータスを変更しても処理が終われば消えてしまいます。常駐化してメモリに格納してしまうのも一つの手ですが、ここでは単純にシングルファイルへ書き込むことでステータスを保存することにします。もちろんこれはシングル実行が前提となりますが、今はモックアップとして一通り動作するものを作ることを優先することにします。
　ただ、ファイルの読み書きでステータスを保存するにしても、googleとはJSON形式でやりとりしますから、データ加工が煩雑になるのも避けたいところです。

　そこで本稿では大きく以下のように進めることにしました。
1. データの表現形式を決める
1. データ操作クラスを作る
1. リライト
1. EXECUTIONを処理する
# ２．データの表現形式を決める
　Google Assistantのドキュメント（[Intent fulfillment](https://developers.google.com/assistant/smarthome/develop/process-intents?hl=en )）ではJSON, Node.ja, JAVAの応答例が載っていますが、リクエストがJSONであるということと、ステータスをファイルとして入出力すること、PHPで（比較的）取り扱いしやすいことからJSON形式で扱うことにしました。
　また、SYNCやQUERY要求に対する応答は定型的なものであることから、あまりデータ構造変換を必要としないものが楽そうです。要はファイルから読み込んだデータ構造をそのまま応答として使えるようにしておくと良さそうだ、ということです。
## ２．１．ＪＳＯＮによる表現形式
　EXECUTEの応答とQUERYの応答はほぼ同型のようですからSYNCとQUERYを想定しておけば良さそうです。SYNCはデバイスの存在そのものでQUERYはそれぞれのデバイスが持つ状態を応答すれば良いようです。そこで、データの表現形式としては次のようにしました。
```
{
  "devices":[
    {
      "id":"t001",
      "type":"action.devices.types.OUTLET",
      "willReportState":true,
      "traits":[
        "action.devices.traits.OnOff"
      ],
      "name":{
        "name":"virtual outlet"
      },
      "deviceInfo":{
        "manufacturer":"YOUR DEVICE",
        "model":"mockup",
        "hwVersion":"0.01",
        "swVersion":"0.01"
      }
    }
  ],
  "conditions":{
    "t001":{
      "status":"SUCCESS",
      "online":true,
      "on":false
    }
  }
}
```
　構造としては大きくdevicesとconditionsに分かれ、devicesはSYNC応答、conditionsはQUERY(EXECUTE)応答用になります。ただ、このままデータ操作のロジックが表に出てくるとそれはそれで読みづらくなりそうなので、データ操作用のクラスに押し込んで取り扱うこととしました。
## ２．２．データ操作クラスを作る
　データ操作クラスとしては以下の機能を持たせることにしました。
1 SYNC応答とQUERY応答をプロパティとして取れるようにする
2 指定したデバイスＩＤの状態を取れるようにする
3 指定したデバイスＩＤの状態を変更し、保存できるようにする
　グーグルアシスタントからのリクエストを見てデバッグしつつ作成したので手直しの余地がだいぶありますが、以下のようになりました。
```
class smaDevice{
   private $jsonFile;
   private $base;
   public  $devices;
   public  $conditions;

  public function __construct($fname){
     $this->jsonFile = $fname;
     $this->base = json_decode(file_get_contents($fname), true);
     $this->devices = $this->base['devices'];
     $this->conditions = $this->base['conditions'];
   }
   
  public function getCondition($deviceId){
    foreach($this->conditions as $id=>$arrStat){
      if($id == $deviceId) return $arrStat;
    }
    return null;
  }
  
  private function setCondition($deviceId, $param, $val){
      switch($param){
        case 'on':
          $this->base['conditions'][$deviceId]['on'] = ($val == true);  // true(1)/false()
          $this->conditions = $this->base['conditions'];
          $this->flushDeviceJson();
        break;
      }
  }

  private function flushDeviceJson(){
    file_put_contents($this->jsonFile, json_encode($this->base));
  }

  public function execCmdToDevice($arrDev, $arrCmd){
    $max = count($arrDev);
    for($i=0;$i<$max;$i++){
      $id      = $arrDev[$i]->id;
      $command = $arrCmd[$i]->command;
      $params  = $arrCmd[$i]->params;
      foreach($params as $prm => $val){
        $this->setCondition($id, $prm, $val);
      }
    }
  }
}
```
## ２．３．SYNCとQUERY応答を手直しする
　作成したデータ操作クラスを使って前回作成（[スマートホームデバイス的な何かを作る](https://qiita.com/SSaitou/items/ec0b3a252934a371a82e )）した応答ロジックをリライトしてみます。
　SYNC応答は次のようになりました。
```
  objDevices = new smaDevice("device.json");
  $jsnIntent = json_decode($jsonBody);
  $intReqId = $jsnIntent->requestId;
　　:
　　:
  $objRes = array(
            "requestId" => $intReqId,
            "payload"   => array(
               "agentUserId" => "userMockup",
               "devices" => $objDevices->devices
             )     // payload
           );      // objRes
```
　QUERY応答は次のようになります。
```
  objDevices = new smaDevice("device.json");
  $jsnIntent = json_decode($jsonBody);
  $intReqId = $jsnIntent->requestId;
　　:
　　:
  $objRes = array(
            "requestId" => $intReqId, 
            "payload" => array(
              "devices" => $objDevices->conditions
             )
          );
```
　感覚的ですが、トップレベルのロジックはgoogleからのリクエストとその応答をエンベローブとして扱い、中身についてはデータ操作クラスに押し込んだ格好になります。
# ３．EXECUTEを処理する（簡単に）
　ここでEXECUTEを処理するロジックを追加します。EXECUTE要求では、操作対象のデバイスＩＤと操作種別、操作に際してのパラメータが送られてきます。ただ、実際の要求ではデバイスと操作内容はそれぞれ配列として送られてくるようなので、受け取った要求からデバイスＩＤと操作内容のペアを取り出して、それぞれのデバイスが持つ状態を変更するような作りにしました。また、状態を変更したら元のJSONファイルに書き戻してしまいます。
（今のところ）処理の流れとしては`execCmdToDevice`（EXECUTEリクエストの分解）→`setCondition`（個々の操作）→`flushDeviceJson`（JSONファイルへの書き出し）となります。`flushDeviceJson`メソッドの呼び出し位置は`execCmdToDevice`の中の方が良さそうです（が、本稿ではそのまま）。
　トップレベルでは次のようになります。
```
  objDevices = new smaDevice("device.json");
　　:
　　:
     case "action.devices.EXECUTE":
       $arrTgt = $jsnIntent->inputs[0]->payload->commands[0]->devices;
       $arrCmd = $jsnIntent->inputs[0]->payload->commands[0]->execution;
       $objDevices->execCmdToDevice($arrTgt, $arrCmd);

       $objRes = array(
                 "requestId" => $intReqId, 
                 "payload" => array(
                   "devices" => $objDevices->conditions
                  )
               );
       $strRes = json_encode($objRes);
     break;
```
　リクエストで送られてきたJSONのデータ構造からターゲットになるデバイスＩＤの配列と操作内容の配列を取り出し、`execCmdToDeivce`でローカルにJSON形式で持たせている状態ファイルを更新＆保存してから状態を応答します。今は持たせている仮想デバイスが１つだけなので応答はシンプルですが、複数のデバイスを持った場合はもうちょっと工夫が必要そうです。
# ４．おわりに
　アプリから疑似アウトレットのオン・オフ操作をして、ローカルに作成したJSONファイルの"on"ステータスがtrue/falseで変更されることが確認できました。これで一応アプリからサーバーの何らかの操作が可能になります（ただし、シングル操作のみ）。
　これで何ができるんだという話はありますが、rasipberryPiに繋いだリレー操作くらいは実現できそうです。次は操作対象の疑似デバイスを増やす、というのと複数の操作を持つデバイスを登録することを進めたいと思っています。