AWS Greengrass V2のShadowManagerでClassic Shadowと同期する方法

Greengrass V2でClassic Shadowを使う方法があまり情報無かったので書いておきます。

`test-device-001` というThingを登録した前提で話を進めます。


## Componentの設定
AWSの以下のドキュメントを頼りに設定します。

https://docs.aws.amazon.com/greengrass/v2/developerguide/sync-shadows-with-iot-core.html

https://docs.aws.amazon.com/greengrass/v2/developerguide/shadow-manager-component.html

いかに`aws.greengrass.ShadowManager` のコンポーネントの設定を記載しています。

```component.json
{
  "aws.greengrass.Cli": {
    "componentVersion": "2.9.1"
  },
...省略

  "aws.greengrass.ShadowManager": {
    "componentVersion": "2.3.0",
    "configurationUpdate": {
          "merge": "{\"rateLimits\":{\"maxLocalRequestsPerSecondPerThing\":20,\"maxOutboundSyncUpdatesPerSecond\":100,\"maxTotalLocalRequestsRate\":200},\"shadowDocumentSizeLimitBytes\":8192,\"shadowDocuments\":[{\"classic\":true,\"thingName\":\"test-device-001\"}],\"strategy\":{\"type\":\"realTime\"},\"synchronize\":{\"coreThing\":{\"classic\":true},\"direction\":\"betweenDeviceAndCloud\"}}"
    }
  },
...省略

}
```

Classic Shadowを使う上で重要な部分は　`merge` のところに入っています。 以下のような内容になります。デプロイ後に確認したYAMLから見ると以下の内容になります。

`synchronize.coreThing.classic = true` と`shadowDocuments.shadowDocuments` の`thingName` ごとの設定に `classic = true` を入れているのがポイントです。

```yaml
---
componentType: "PLUGIN"
configuration:
  rateLimits:
    maxLocalRequestsPerSecondPerThing: 20
    maxOutboundSyncUpdatesPerSecond: 100
    maxTotalLocalRequestsRate: 200
  shadowDocuments:
  - classic: true
    thingName: "test-device-001"
  shadowDocumentSizeLimitBytes: 8192
  strategy:
    type: "realTime"
  synchronize:
    coreThing:
      classic: true
    direction: "betweenDeviceAndCloud"
dependencies:
- "aws.greengrass.Nucleus:SOFT"
lifecycle: {}
version: "2.3.0"
```

## Pythonスクリプトの例
起動時に一度だけClassic Shadowを更新して、そのShadowの内容を読み込む簡単なスクリプトを作ってみました。

ポイントとなるのは `shadow_name=""` の部分です。空文字がClassic Shadowを表すようです。

参考： https://github.com/aws-greengrass/aws-greengrass-shadow-manager/blob/v2.3.0/src/integrationtests/java/com/aws/greengrass/integrationtests/SyncDirectionalityTest.java#L59

```python
import json
from typing import Optional, Union
from traceback import format_exc
import awsiot.greengrasscoreipc
from awsiot.greengrasscoreipc.model import (
    GetThingShadowRequest,
    UpdateThingShadowRequest,
)

THING_NAME = "test-device-001"


def update_default_shadow(
    thing_name: str, payload: Optional[Union[bytes, str]]
) -> Optional[dict]:
    try:
        ipc_client = awsiot.greengrasscoreipc.connect()
        op = ipc_client.new_update_thing_shadow()
        op.activate(
            UpdateThingShadowRequest(
                thing_name=thing_name,
                shadow_name="",  # Classic Shadow
                payload=payload,
            )
        )
        future_response = op.get_response()
        result = future_response.result(timeout=10)
        if result.payload:
            return json.loads(result.payload)
        return None

    except Exception as e:
        print(format_exc())
        print("Error update shadow", type(e), e)
        raise e


def read_default_shadow(thing_name: str) -> Optional[dict]:
    try:
        ipc_client = awsiot.greengrasscoreipc.connect()
        op = ipc_client.new_get_thing_shadow()
        op.activate(
            request=GetThingShadowRequest(
                thing_name=thing_name,
                shadow_name="",  # Classic Shadow
            )
        )
        future_resp = op.get_response()
        result = future_resp.result(timeout=10)
        if result.payload:
            return json.loads(result.payload)
        return None
    except Exception as e:
        print(format_exc())
        print("Error read shadow", type(e), e)
        raise e


update_res = update_default_shadow(
    thing_name=THING_NAME,
    payload=bytes(json.dumps({"state": {"reported": {"status": "OK"}}}), "utf-8"),
)
update_message = f"UPDATE -> {update_res}"
print(update_message)

read_res = read_default_shadow(thing_name=THING_NAME)
message = f"READ -> {read_res}"
print(message)
```

## Recipeの例
後は以下のようなRecipeでデプロイすればOKです。　`com.hello.shadow`　というComponent名で登録してみました。

```recipe.json
{
  "RecipeFormatVersion": "2020-01-25",
  "ComponentName": "com.hello.shadow",
  "ComponentVersion": "1.0.0",
  "ComponentDescription": "Hello Shadow",
  "ComponentPublisher": "Me",
  "ComponentConfiguration": {
    "DefaultConfiguration": {
      "accessControl": {
        "aws.greengrass.ShadowManager": {
          "com.hello.shadow:shadow:1": {
            "policyDescription": "Allows access to shadows",
            "operations": [
              "aws.greengrass#GetThingShadow",
              "aws.greengrass#UpdateThingShadow",
              "aws.greengrass#DeleteThingShadow"
            ],
            "resources": [
              "$aws/things/test-device-001/shadow"
            ]
          }
        }
      }
    }
  },
  "Manifests": [
    {
      "Platform": {
        "os": "linux"
      },
      "Lifecycle": {
        "Install": {
          "RequiresPrivilege": true,
          "script": "python3 -m pip install --user awsiotsdk"
        },
        "Run": {
          "RequiresPrivilege": true,
          "script": "python3 -u {artifacts:path}/main.py "
        }
      }
    }
  ]
}
```


## 実行してみる


Greengrass CLIから以下のように実行してみます。

```sh
sudo /greengrass/v2/bin/greengrass-cli component restart -n "com.hello.shadow"
```

ログを見るとShadowを更新して、読み込んでいるところが確認できます。

```
2023-03-13T11:20:15.997Z [INFO] (pool-2-thread-48) com.hello.shadow: shell-runner-start. {scriptName=services.com.hello.shadow.lifecycle.Run.script, serviceName=com.hello.shadow, currentState=STARTING, command=["python3 -u /greengrass/v2/packages/artifacts/com.hello.shadow/1.0.0/main.py "]}
2023-03-13T11:20:16.176Z [INFO] (Copier) com.hello.shadow: stdout. UPDATE -> b'{"version":1,"timestamp":1678706416,"state":{"reported":{"status":"OK"}},"metadata":{"reported":{"status":{"timestamp":1678706416}}}}'. {scriptName=services.com.hello.shadow.lifecycle.Run.script, serviceName=com.hello.shadow, currentState=RUNNING}
2023-03-13T11:20:16.194Z [INFO] (Copier) com.hello.shadow: stdout. READ -> {'state': {'reported': {'status': 'OK'}}, 'metadata': {'reported': {'status': {'timestamp': 1678706416}}}, 'version': 1, 'timestamp': 1678706416}. {scriptName=services.com.hello.shadow.lifecycle.Run.script, serviceName=com.hello.shadow, currentState=RUNNING}
2023-03-13T11:20:16.229Z [INFO] (Copier) com.hello.shadow: Run script exited. {exitCode=0, serviceName=com.hello.shadow, currentState=RUNNING}
```

AWS側を見るとClassic Shadowが登録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116068/2b7cee5a-5c80-bf48-e3d3-cac5861f550f.png)


Shadowの中身も更新されてそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116068/957acafb-5ca7-8284-f8c7-c2413679c6e3.png)