ラズパイで部屋の環境(温度湿度気圧CO2)をモニタリングするIoTデバイスを作る④ ～Reactで可視化ダッシュボード作成～

## はじめに
この記事は下記記事の続きとなっていますので先にこちらをご覧ください。

https://qiita.com/h0uk1st4r/items/5f206e64744a3724f5ef

本記事ではDynamoDBに蓄えたセンサーデータをReactApp(WEBアプリ)で可視化できるようにします。

## 構成
図のようにDynamoDBよりセンサーデータを取得するためにAPI Gateway、取得したデータの可視化にS3ホスティングされたReactAppを構築します。

![EnvDataStack構成図V2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3870591/70d80bf4-6609-652e-adc3-594615a08e24.png)

## API用Lambda関数
API用Lambda関数のコードとrequirements.txtをSAMプロジェクトのapi_srcディレクトリ内に配置します。
```python:api_src/lambda_function.py
import os
from typing import Literal
from decimal import Decimal

import boto3
from mangum import Mangum
from fastapi import FastAPI
from pydantic import BaseModel
from botocore.config import Config
from boto3.dynamodb.types import TypeDeserializer

class SensorDataItemSchema(BaseModel):
    Timestamp: str
    Value: Decimal

dynamodb_client = boto3.client(
    "dynamodb",
    config=Config(
        retries={
            "max_attempts": 5,
            "mode": "standard"
        }
    )
)
table_arn: str = os.environ["TABLE_ARN"]

api: FastAPI = FastAPI()
@api.get("/{sensor_type}/data", response_model=list[SensorDataItemSchema])
def get_sensor_data(sensor_type: Literal["concentration", "temperature","humidity","pressure"]):
    response: dict = dynamodb_client.query(**{
        "TableName": table_arn,
        "KeyConditionExpression": "#Type = :type",
        "ExpressionAttributeNames": {
            "#Type": "Type"
        },
        "ExpressionAttributeValues": {
            ":type": {"S": sensor_type}
        }
    })
    items: list[SensorDataItemSchema] = [
        SensorDataItemSchema(**{
                k: TypeDeserializer().deserialize(v)
                for k, v in item.items()
            }
        )
        for item in response.get("Items", [])
    ]
    return items

lambda_handler: Mangum = Mangum(api)

```

```text:api_src/requirements.txt
annotated-types==0.7.0
anyio==4.6.2.post1
exceptiongroup==1.2.2
fastapi==0.115.5
idna==3.10
mangum==0.19.0
pydantic==2.10.2
pydantic_core==2.27.1
sniffio==1.3.1
starlette==0.41.3
typing_extensions==4.12.2

```

## 可視化ダッシュボード用ReactAppの構築
1. `create-react-app`コマンドでReactAppを作成します
    ```shell
    >npx create-react-app env-data-viewer --template typescript
    ```
1. `npm install`コマンドで必要なライブラリ(axios、MaterialUI関連、date-fns)をインストールします
    ```shell
    >npm install axios
    >npm install @mui/material @emotion/react @emotion/styled @mui/icons-material @mui/x-charts
    >npm install date-fns
    ```
1. `App.tsx`を次のように書き換えます
    ```react:App.tsx
    import axios from 'axios';
    import { format } from 'date-fns';
    import { Grid2, Typography } from '@mui/material';
    import { useState, useEffect, type FC } from 'react';
    import { axisClasses } from '@mui/x-charts/ChartsAxis';
    import { LineChart, type ShowMarkParams } from '@mui/x-charts';

    interface SensorDataItemSchema {
      Timestamp: Date;
      Value: number;
    }

    const getSensorData = async (sensorType: string): Promise<SensorDataItemSchema[]> => {
      const url = `${process.env.REACT_APP_API_ENDPOINT}/${sensorType}/data`;
      try {
        const response = await axios.get(url);
        return response.data.map((item: {Timestamp: string, Value: string}) => ({
          Timestamp: new Date(item.Timestamp),
          Value: parseFloat(item.Value)
        }))
      } catch (error) {
        console.log("API Fail, reason:", error);
        return [];
      }
    }

    const dateFormatter = (value: Date) => {
      return format(value, "dd日 HH:mm");
    }

    const showMark = (params: ShowMarkParams) => {
      const { position } = params as ShowMarkParams<Date>;
      return [0, 6, 12, 18].includes(position.getHours()) && position.getMinutes() === 0
    };

    const SensorDataChart: FC<{ sensorType: string, displayName: string, unit: string}> = ({ sensorType, displayName, unit }): JSX.Element => {
      const [ sensorData, setSensorData ] = useState<SensorDataItemSchema[]>([]);
      const fetchSensorData = async () => {
        const response = await getSensorData(sensorType);
        setSensorData(response);
      }

      useEffect(() => {
        fetchSensorData();
      }, []);

      return (
        <Grid2
          size={{
            xs: 12,
            lg: 6
          }}
        >
          <Typography>
            {displayName}
          </Typography>
          <LineChart
            xAxis={[{
              data: sensorData.map((item) => (item.Timestamp)),
              scaleType: "time",
              label: "日時",
              valueFormatter: (value) => dateFormatter(value),
              tickInterval: (time) => time.getMinutes() === 0,
              tickLabelInterval: (time) => time.getMinutes() === 0
            }]}
            yAxis={[{
              label: `${displayName} (${unit})`
            }]}
            series={[{
              data: sensorData.map((item) => (item.Value)),
              valueFormatter: (value) => `${value} ${unit}`,
              showMark
            }]}
            grid={{horizontal: true}}
            height={400}
            margin={{
              left: 96
            }}
            sx={{
              [`& .${axisClasses.left} .${axisClasses.label}`]: {
                transform: 'translateX(-32px)',
              }
            }}
          />
        </Grid2>
      )
    }

    const App: FC = (): JSX.Element => {
      return (
        <Grid2
          container
          spacing={2}
          sx={{
            padding: 2
          }}
        >
          <SensorDataChart
            sensorType="temperature"
            displayName="温度"
            unit="℃"
          />
          <SensorDataChart
            sensorType="concentration"
            displayName="CO2濃度"
            unit="ppm"
          />
          <SensorDataChart
            sensorType="humidity"
            displayName="湿度"
            unit="%"
          />
          <SensorDataChart
            sensorType="pressure"
            displayName="気圧"
            unit="hPa"
          />
        </Grid2>
      )
    }

    export default App;
    ```
## SAMによるアプリケーションのデプロイ
SAMを使用してAPI Gateway、API用Lambda関数、S3バケットを追加でデプロイします。
1. template.yamlを次のように修正します
    ```yaml:template.yaml
    AWSTemplateFormatVersion: "2010-09-09"
    Transform: "AWS::Serverless-2016-10-31"
    Description: "EnvDataStack"

    Resources:
      EnvDataTable:  # データ格納用DynamoDBテーブル
        Type: "AWS::DynamoDB::Table"
        Properties:
          AttributeDefinitions:
            - AttributeName: "Type"
              AttributeType: "S"
            - AttributeName: "Timestamp"
              AttributeType: "S"
          BillingMode: "PROVISIONED"
          DeletionProtectionEnabled: true
          KeySchema:
            - AttributeName: "Type"
              KeyType: "HASH"
            - AttributeName: "Timestamp"
              KeyType: "RANGE"
          ProvisionedThroughput:
            ReadCapacityUnits: 1
            WriteCapacityUnits: 1
          TableName: "EnvDataTable"
          TimeToLiveSpecification:
            AttributeName: "TTL"
            Enabled: true

      EnvDataPusher:  # データ格納用Lambda関数
        Type: "AWS::Serverless::Function"
        Properties:
          AssumeRolePolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Principal:
                  Service:
                    - "lambda.amazonaws.com"
                Action:
                  - "sts:AssumeRole"
          AutoPublishAlias: "Alias"
          CodeUri: "src/"
          Description: "iot-env-data-collectorのデバイスシャドウ更新に応じて最新のデータをDynamoDBにプッシュする関数"
          Environment:
            Variables:
              TABLE_ARN: !GetAtt "EnvDataTable.Arn"
          Events:
            ShadowUpdateAccepted:
              Type: "IoTRule"
              Properties:
                Sql: "SELECT * FROM \"$aws/things/iot-env-data-collector/shadow/update/accepted\""
          FunctionName: "EnvDataPusher"
          Handler: "lambda_function.lambda_handler"
          MemorySize: 128
          Policies:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "*"
              - Effect: "Allow"
                Action:
                  - "dynamodb:PutItem"
                Resource: !GetAtt "EnvDataTable.Arn"
          Runtime: "python3.9"
          Timeout: 30

      EnvDataViewerBucket:  # 可視化ダッシュボードS3ホスティングバケット
        Type: "AWS::S3::Bucket"
        Properties:
          BucketName: "env-data-viewer"
          PublicAccessBlockConfiguration:
            BlockPublicAcls: false
            BlockPublicPolicy: false
            IgnorePublicAcls: false
            RestrictPublicBuckets: false
          WebsiteConfiguration:
            IndexDocument: "index.html"

      EnvDataViewerBucketPolicy:  # バケットポリシー
        Type: "AWS::S3::BucketPolicy"
        Properties:
          Bucket: !Ref "EnvDataViewerBucket"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action: "s3:GetObject"
                Effect: "Allow"
                Resource: !Sub
                  - "${arn}/*"
                  - arn: !GetAtt "EnvDataViewerBucket.Arn"
                Principal: "*"

      EnvDataHTTPAPI:  # データ取得用HTTP API
        Type: "AWS::Serverless::HttpApi"
        Properties:
          CorsConfiguration:
            AllowCredentials: false
            AllowHeaders:
              - "content-type"
            AllowMethods:
              - "*"
            AllowOrigins:
              - !GetAtt "EnvDataViewerBucket.WebsiteURL"
            MaxAge: 0
          Description: "センサーデータ取得用HTTP API"
          Name: "EnvDataHTTPAPI"

      EnvDataHTTPAPILambda:  # データ取得用HTTP API Lambda関数
        Type: "AWS::Serverless::Function"
        Properties:
          AssumeRolePolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Principal:
                  Service:
                    - "lambda.amazonaws.com"
                Action:
                  - "sts:AssumeRole"
          AutoPublishAlias: "Alias"
          CodeUri: "api_src/"
          Description: "センサーデータ取得用HTTP API Lambda関数"
          Environment:
            Variables:
              TABLE_ARN: !GetAtt "EnvDataTable.Arn"
          Events:
            GetSensorData:
              Type: "HttpApi"
              Properties:
                ApiId: !Ref "EnvDataHTTPAPI"
                Method: "GET"
                Path: "/{sensor_type}/data"
          FunctionName: "EnvDataHTTPAPI"
          Handler: "lambda_function.lambda_handler"
          MemorySize: 128
          Policies:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "*"
              - Effect: "Allow"
                Action:
                  - "dynamodb:Query"
                Resource: !GetAtt "EnvDataTable.Arn"
          Runtime: "python3.9"
          Timeout: 30

    ```
1. `sam build`でデプロイの準備をします
    ```shell
    [EnvDataStack]$ sam build
    Starting Build use cache                                                                                                                                                                                                                                                             
    Building codeuri: /tmp/EnvDataStack/src runtime: python3.9 architecture: x86_64 functions: EnvDataPusher                                                                                                                                                                             
    Manifest file is changed (new hash: df68a8bca38f0002a68e9384d43588c6) or dependency folder (.aws-sam/deps/420c2445-345f-4edd-bd82-288634f1fd0e) is missing for (EnvDataHTTPAPILambda), downloading dependencies and copying/building source                                          
    Building codeuri: /tmp/EnvDataStack/api_src runtime: python3.9 architecture: x86_64 functions: EnvDataHTTPAPILambda                                                                                                                                                                  
    requirements.txt file not found. Continuing the build without dependencies.                                                                                                                                                                                                          
    Running PythonPipBuilder:CopySource                                                                                                                                                                                                                                                 
    Running PythonPipBuilder:CleanUp                                                                                                                                                                                                                                                    
    Running PythonPipBuilder:ResolveDependencies                                                                                                                                                                                                                                        
    Running PythonPipBuilder:CopySource                                                                                                                                                                                                                                                 
    Running PythonPipBuilder:CopySource                                                                                                                                                                                                                                                 

    Build Succeeded

    Built Artifacts  : .aws-sam/build
    Built Template   : .aws-sam/build/template.yaml

    Commands you can use next
    =========================
    [*] Validate SAM template: sam validate
    [*] Invoke Function: sam local invoke
    [*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
    [*] Deploy: sam deploy --guided
    ```
1. `sam deploy`でスタックを更新します
    ```shell
    [EnvDataStack]$ sam deploy
    ～省略～
    Previewing CloudFormation changeset before deployment
    ======================================================
    Deploy this changeset? [y/N]: y
    ～省略～
    CloudFormation events from stack operations (refresh every 5.0 seconds)
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    ResourceStatus                                                       ResourceType                                                         LogicalResourceId                                                    ResourceStatusReason                                               
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    UPDATE_IN_PROGRESS                                                   AWS::CloudFormation::Stack                                           EnvDataStack                                                         User Initiated                                                     
    CREATE_IN_PROGRESS                                                   AWS::S3::Bucket                                                      EnvDataViewerBucket                                                  -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::IAM::Role                                                       EnvDataHTTPAPILambdaRole                                             -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::S3::Bucket                                                      EnvDataViewerBucket                                                  Resource creation Initiated                                        
    CREATE_IN_PROGRESS                                                   AWS::IAM::Role                                                       EnvDataHTTPAPILambdaRole                                             Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::S3::Bucket                                                      EnvDataViewerBucket                                                  -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::S3::BucketPolicy                                                EnvDataViewerBucketPolicy                                            -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::S3::BucketPolicy                                                EnvDataViewerBucketPolicy                                            Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::S3::BucketPolicy                                                EnvDataViewerBucketPolicy                                            -                                                                  
    CREATE_COMPLETE                                                      AWS::IAM::Role                                                       EnvDataHTTPAPILambdaRole                                             -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Function                                                EnvDataHTTPAPILambda                                                 -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Function                                                EnvDataHTTPAPILambda                                                 Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::Lambda::Function                                                EnvDataHTTPAPILambda                                                 -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Version                                                 EnvDataHTTPAPILambdaVersion758a4c335e                                -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Version                                                 EnvDataHTTPAPILambdaVersion758a4c335e                                Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::Lambda::Version                                                 EnvDataHTTPAPILambdaVersion758a4c335e                                -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Alias                                                   EnvDataHTTPAPILambdaAliasAlias                                       -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Alias                                                   EnvDataHTTPAPILambdaAliasAlias                                       Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::Lambda::Alias                                                   EnvDataHTTPAPILambdaAliasAlias                                       -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::ApiGatewayV2::Api                                               EnvDataHTTPAPI                                                       -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::ApiGatewayV2::Api                                               EnvDataHTTPAPI                                                       Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::ApiGatewayV2::Api                                               EnvDataHTTPAPI                                                       -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::ApiGatewayV2::Stage                                             EnvDataHTTPAPIApiGatewayDefaultStage                                 -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Permission                                              EnvDataHTTPAPILambdaGetSensorDataPermission                          -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::Lambda::Permission                                              EnvDataHTTPAPILambdaGetSensorDataPermission                          Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::Lambda::Permission                                              EnvDataHTTPAPILambdaGetSensorDataPermission                          -                                                                  
    CREATE_IN_PROGRESS                                                   AWS::ApiGatewayV2::Stage                                             EnvDataHTTPAPIApiGatewayDefaultStage                                 Resource creation Initiated                                        
    CREATE_COMPLETE                                                      AWS::ApiGatewayV2::Stage                                             EnvDataHTTPAPIApiGatewayDefaultStage                                 -                                                                  
    UPDATE_COMPLETE_CLEANUP_IN_PROGRESS                                  AWS::CloudFormation::Stack                                           EnvDataStack                                                         -                                                                  
    UPDATE_COMPLETE                                                      AWS::CloudFormation::Stack                                           EnvDataStack                                                         -                                                                  
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


    Successfully created/updated stack - EnvDataStack in ap-northeast-1
    ```

## ReactAppをビルドしS3バケットに配置する
1. ReactAppの`.env`ファイルに作成されたAPI Gatewayのエンドポイントを記述します
    ```text:.env
    REACT_APP_API_ENDPOINT=https://8XXXXXXXXl.execute-api.ap-northeast-1.amazonaws.com
    ```
1. `npm run build`コマンドでReactAppをビルドします
    ```shell
    >npm run build
    ```
1. ビルドされた`build`フォルダ内のファイル・フォルダ一式を作成されたS3バケットにアップロードします
    
    ![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3870591/176efee8-98a4-cfbd-22b3-2c0f99763fb7.png)

## 可視化ダッシュボードの完成
S3バケットのバケットウェブサイトエンドポイントを開くと可視化ダッシュボードが表示され、温度湿度気圧CO2のセンサーデータが時系列データとして確認することができます。

![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3870591/f534ba52-bd81-c9cb-9a63-621fe2b31f36.png)

## さいごに
IoT Coreに送信されたセンサーデータをDynamoDBに蓄え、ReactAppから蓄えたデータをAPIで取得しグラフィカルに表示できるようになりました。

SSL化やCognitoを利用したユーザ認証など付け加えたいものはありますが、一連の記事はここまでにすることにします。
記事中端折った部分も多分にありますが誰かのIoTデバイス作りの参考になれば幸いです。