【Spring-boot】Nature Remoをローカル環境から操作する【React】

# NatureRemo
テレビやエアコン等の赤外線操作ができる家電をスマホやスマートスピーカー等から操作できるようにするデバイス。
[Nature Remo（ネイチャーリモ）](https://nature.global/nature-remo/ "https://nature.global/nature-remo/")

# 概要
たまにAWSの障害が波及してNatureRemoが使えなくなる事象が発生する。
こうなってしまうと、いつもはAlexaで操作していた家電の切り替えがリモコンじゃないと操作できなくなる。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">昨晩深夜からのNature Remoのシステム障害ですが、AWS（アマゾンウェブサービス）の障害に起因しており、現時点で一部改善はしておりますが、完全復旧までにはもう少し時間がかかる状況です。<br>ご不便をおかけて大変申し訳ございません。今しばらく復旧をお待ちいただけますと幸いです。</p>&mdash; Nature株式会社 (@NatureRemo_JP) <a href="https://twitter.com/NatureRemo_JP/status/1331720497965350913?ref_src=twsrc%5Etfw">November 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

また、NatureRemoはLocal環境からアクセスできるAPIを用意していて赤外線信号の取得と実行のみができる仕組みがある。
これを利用してローカル環境だけでNatureRemoを操作できるアプリの開発を行う。
赤外線信号を保存する仕組みは備わっていない様子。こちらも合わせて実装していく。
[Nature Inc](https://developer.nature.global/ "https://developer.nature.global/")

# 環境整理
- サーバ: Spring-boot
- フロントエンド: React.js
- DB: Cassandra

# サーバサイド
[akapo001/local-remo](https://github.com/akapo001/local-remo "https://github.com/akapo001/local-remo")
サーバサイドはREST APIをSpringBootで実装する。
firebaseで実装しても良かったが次のアサイン予定のプロジェクトでSpringを使う予定なので思い出すのを兼ねて自前で実装を行った。
DBはRDBで実装するとNatureRemoから取得する赤外線情報のフォーマット的に大変そうだったのでNoSQLのCassandraを使用してデータの保存を行う。
CassandraはDockerのイメージを利用して構築する。

## API一覧
- 信号取得
    - NatureRemoから赤外線信号を取得する
    - NatureRemoに赤外線信号を当てたあとこのAPIを実行すると赤外線情報を取得できる
- 信号保存
    - 信号取得で取得した赤外線情報をそのままこのAPIのBodyにわたすとサーバ側のCassandraに保存するAPI
    - パスパラメータに信号に紐付ける名前を指定する。
- 信号一覧取得
    - Cassandraに保存されている信号の一覧を取得する。
- 信号実行
    - 信号一覧で取得したデータから実行したい信号のIDを設定してリクエストするとNatureRemoを通して家電の操作ができる。
- 信号削除
    - 信号一覧で取得したデータから削除したい信号のIDを設定してリクエストするとCassandraからデータを削除する。

## SpringからCassandraへの接続設定
SpringからCassandra呼び出しをする処理のサンプルがなくて苦労したが下記のクラスをConfigrationクラスとして定義することで接続ができた。
Keyspaceやホスト、ポート等は設定ファイルから読み取る。

```java
@Configuration
@EnableCassandraRepositories(cassandraTemplateRef = "cassandraTemplate")
public class CassandraConfig extends AbstractCassandraConfiguration {
    @Value("${spring.data.cassandra.username}")
    private String username;

    @Value("${spring.data.cassandra.password}")
    private String password;

    @Value("${spring.data.cassandra.keyspace-name}")
    private String keyspaceName;

    @Value("${spring.data.cassandra.contact-points}")
    private String contactPoints;

    @Value("${spring.data.cassandra.port}")
    private int port;


    @Override
    @Primary
    @Bean
    public CassandraAdminTemplate cassandraTemplate() {
        CqlSession session = CqlSession.builder().withKeyspace(keyspaceName).build();
        return new CassandraAdminTemplate(session);
    }

    // ポート番号の設定
    @Override
    @Bean(name = "Port")
    public int getPort() {
        return port;  // デフォルトは9042
    }

    // キースペースの設定
    @Override
    @Bean(name = "KeySpace")
    protected String getKeyspaceName() {
        return keyspaceName;
    }
    
}
```

## Cassandra操作
こちらも最新のバージョンだとAPIのIFが違うらしくなかなかアクセスができなかったので以下にサンプルを記載

```java
@Service
public class CassandraService {

    @Autowired
    CassandraAdminTemplate cassandraTemplate;

    public SignalTable insert(Signal signal, String name) {
        return cassandraTemplate.insert(new SignalTable(Uuids.timeBased(), name, signal));
    }

    public List<SignalTable> selectAll() {
        SimpleStatement select = QueryBuilder.selectFrom("signal").all().build();
        return cassandraTemplate.select(select, SignalTable.class);
    }

    public SignalTable select(String id) {
        UUID uuid = UUID.fromString(id);
        return cassandraTemplate.selectOneById(uuid, SignalTable.class);
    }

    public String delete(String id) throws NotFoundSignal {
        UUID uuid = UUID.fromString(id);
        Boolean res = cassandraTemplate.deleteById(uuid, SignalTable.class);
        if(!res) throw new NotFoundSignal("信号が見つかりませんでした");

        return id;
    }

}

```

## CORS対策
ローカルのフロントエンドからローカルに別途立てたサーバを呼び出したところ下記のエラーが出た。

```
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
```

controllerクラスにCrossOriginアノテーションを追加してやることで回避できた。
デフォルトだとすべてのアクセスを許可する。

```java
@RestController
@CrossOrigin
public class SignalController {
    @Autowired
    SignalService signalService;
    @Autowired
    CassandraService cassandraService;

    @GetMapping("/signal")
    Signal getSignal() {
        return signalService.fetchSignal();
    }

    @PostMapping("/signal")
    Signal sendSignal(@RequestBody SendSignal sendSignal) {
        Signal signal = cassandraService.select(sendSignal.getId()).getSignal();
        return signalService.sendSignal(signal);
    }

    @PostMapping("/signal/{name}")
    SignalTable  postSignal(@RequestBody Signal signal, @PathVariable String name) {return cassandraService.insert(signal, name);}

    @GetMapping("/all-signal")
    List<SignalTable> getAllSignal() { return cassandraService.selectAll();}

    @DeleteMapping("/signal/{id}")
    SimpleResponse deleteSignal(@PathVariable String id) throws NotFoundSignal {
        System.out.println(id);
        cassandraService.delete(id);
        return new SimpleResponse("削除成功");
    }
}
```
# フロントエンド
[akapo001/local-remo-react](https://github.com/akapo001/local-remo-react "https://github.com/akapo001/local-remo-react")
React.jsで上記のAPIを呼び出すような機能を実装
フロントエンドは簡素な作りなのでソース等の詳細は割愛。

## 画面一覧
### Home
画面表示時に信号一覧取得APIを呼び出してリストで表示する。  

- リストには実行ボタンと削除ボタンがありそれぞれ信号実行APIと信号削除APIを呼び出す。
- 登録ボタンをクリックすると下記のCreate画面に遷移する
- 信号一覧のリストはStateで管理して削除ボタンがクリックされた際にはサーバにリクエストを送ったあとにStateからも削除する。
![スクリーンショット 2021-03-08 11.55.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226241/6862b928-368b-d867-9eda-39bfcac7e1e5.png)

### Create
- 信号取得と登録ボタンからそれぞれ信号取得APIと信号保存APIを呼び出す。
- 登録ボタンクリック後に保存が成功すればHome画面に遷移する。
![スクリーンショット 2021-03-08 11.51.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226241/193e4e67-2f5d-cf7c-f6f5-da7836491e2e.png)

### iPhoneからのアクセス
Spring側の起動IPとReact側のアクセスIPをローカルIPにしてあげればiPhoneからでもアクセスが可能です。
<img width="300px" alt="IMG_6098.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226241/6533b75f-634f-069f-a8cd-b02766a56312.png">
<img width="300px" alt="IMG_6099.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226241/606df945-4761-c238-fc45-1a6c16bda0f8.png">