Google Homeをリモートでしゃべらせる(Websocket使用)

Google Home、面白いですね。

@miso_developさんの[Google Homeでやったことまとめ](https://qiita.com/miso_develop/items/99624915d3d6e41a5fb3)に心動かされて、いくつか写経させていただきました。

[IFTTT](https://ifttt.com)を使用するとLINE送信もできる、とのことで導入したところ、小学生の娘が喜んでLINE送信してくれるようになり、そうなるとGoogle Homeに返答をしゃべらせたくなってきます。
調べてみると、Google Homeに発話させるのは
またまた@miso_developさんの[WebからGoogle Homeを喋らせたり家電操作したりしてみる](https://qiita.com/miso_develop/items/09fcce17162db49dec55)があり、以下の手順でできるようです。

1.Node.jsのライブラリ「google-home-notifier」を使用するとGoogle Homeに発話させることができる。
2.1は自宅LAN内で実行する必要があり、リモートでのトリガーにRealtimeDBのFirebaseを使用する。

自分は自宅向けにSpring Bootでシステム構築してあったので、2のトリガーとなる部分をwebsocketにしてみました。Spring使えるとセキュリティ実装に悩まないことが大きいです。
Spring Securityは圧倒的に便利。

参考：@opengl-8080さんの[Spring Security 使い方メモ　認証・認可](https://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)

#機材
Raspberry PI3
ウサギを飼っているので室内の気温ロガー＆エアコン制御で導入済みでした。

Google Home mini
コストコで¥5,000くらいだったような。

#実装

コードはこちら
https://github.com/ko-aoki/web2Googlehome

Spring Boot 1.5.10で実装しています。

websocket周りの説明は以下を参考にしました。

+ https://spring.io/guides/gs/messaging-stomp-websocket/
Spring公式のGetting Started

+ [Spring Bootキャンプ ハンズオン資料](http://spring-boot-camp.readthedocs.io/ja/latest/index.html)
[はじめてのSpring Boot](https://www.amazon.co.jp/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E3%81%AESpring-Boot%E2%80%95%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%A7%E7%B0%A1%E5%8D%98Java%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA-I%E3%83%BB-BOOKS-%E4%BF%8A%E6%98%8E/dp/4777519694/ref=pd_cp_14_1?_encoding=UTF8&psc=1&refRID=C8A5BQHAZ1J6DRDZYKS8)作者さんの資料。
この資料も本も必読ですね。。

##サーバサイド

WebSocketの設定クラスです。
websocketのエンドポイントを設定、メッセージブローカーを有効にして宛先を設定します。

```java:WebSocketConfig.java
package com.example.web2googlehome;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint");
    }

}

```

通常のWebアプリケーションのコントローラクラスです。
パラメータ"send"つきでPOSTされたら
SenderDtoを`WebSocketConfig.java`で設定した宛先に送信します。

```java:SenderController.java
package com.example.web2googlehome.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/top")
public class SenderController {

    SimpMessagingTemplate simpMessagingTemplate;

    @Autowired
    public SenderController(SimpMessagingTemplate simpMessagingTemplate) {
        this.simpMessagingTemplate = simpMessagingTemplate;
    }

    @GetMapping
    public String top(Model model) throws Exception {
        model.addAttribute("senderDto", new SenderDto());
        return "top";
    }

    @PostMapping(params = "send")
    public String send(SenderDto dto, Model model) throws Exception {
        model.addAttribute("senderDto", dto);
        this.simpMessagingTemplate.convertAndSend("/topic/notification", dto);
        return "top";
    }
}


```

SenderDtoはこんな感じ。

```java:SenderDto.java

package com.example.web2googlehome.controller;

public class SenderDto {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

```

発話のトリガーとなるPOSTを実行するHTMLはthymeleafで。
（もちろん、普通にJavaScriptでメッセージをsendしても。）
「送信」押下で、メッセージを`SenderController.java`のsendメソッドでハンドリングしてくれます。


```html:top.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<div>
    <form action="/top"
          th:object="${senderDto}"
          th:action="@{/top}" method="post">
        <div>
            <label for="message">メッセージ</label>
            <input id="message" type="text" th:field="*{message}"/>
        </div>
        <div class="form-group">
            <button type="submit" class="btn-primary" name="send">送信</button>
        </div>
    </form>
</div>
</body>
</html>

```

##クライアントサイド(Raspberry PI)

"ws://localhost:8080/topic/notification"をsubscribeして、
メッセージ通知されたらgoogle-home-notifierをキックして発話します。
`stompFailureCallback`は切断時の再接続ロジックです。
環境によってはheartbeatしないとすぐ切断しちゃうかも。

```javascript:web2googlehome-client.js
const Stomp = require('stompjs');
const Googlehome = require('google-home-notifier');
const language = 'ja';
var ip = 'xx.xx.xx.xx'; //ここにGoogle HomeのIPを記載

Googlehome.device('', language);
Googlehome.ip(ip, language);

var client;  // stompクライアント

var stompFailureCallback = function (error) {
    console.log('STOMP: ' + error);
    client = Stomp.overWS('ws://localhost:8080/endpoint');
    setTimeout(stompConnect, 5 * 60 * 1000);
    console.log('STOMP: Reconecting in 5 minutes');
};

var stompSuccessCallBack = function (frame) {
    console.log('connected to Stomp');
    client.subscribe('/topic/notification', function (data) {

        var obj = JSON.parse(data.body);
        console.log("received message " + obj.message);

        Googlehome.notify(obj.message , function(res) {
            console.log(res);
        });

    });
}

var stompConnect = function() {
    client = Stomp.overWS('ws://localhost:8080/endpoint');
    console.log('connecting to Stomp');
    client.connect('', '', stompSuccessCallBack, stompFailureCallback);
}

stompConnect();

```

自分がデプロイしている環境だと、50secでコネクション切断してしまうので
heartbeatをこんな感じにしてます。
あとはサービス化して、OS起動時や不慮のプロセス停止に対応。

```javascript:web2googlehome-client.js（heartbeatあり）
const Stomp = require('stompjs');
const Googlehome = require('google-home-notifier');
const language = 'ja';
var ip = 'xx.xx.xx.xx'; //ここにIPを記載

Googlehome.device('', language);
Googlehome.ip(ip, language);

var client;  // stompクライアント
var intervalId; // heartbeatのintervalID

var stompFailureCallback = function (error) {
    console.log('STOMP: ' + error);
    clearInterval(intervalId);
    setTimeout(stompConnect, 5 * 60 * 1000);
    console.log('STOMP: Reconecting in 5 minutes');
};

var stompSuccessCallBack = function (frame) {
    console.log('connected to Stomp');
    client.subscribe('/topic/notification', function (data) {

        var obj = JSON.parse(data.body);
        console.log("received message " + obj.message);

        Googlehome.notify(obj.message , function(res) {
            console.log(res);
        });

    });
    // heartbeat
    intervalId = setInterval(function () {
        client.send('/app','');
    }, 10 * 1000)
}

var stompConnect = function() {
    client = Stomp.overWS('ws://localhost:8080/endpoint');
    client.heartbeat.outgoing = 10000;
    console.log(new Date() + ' connecting to Stomp');
    client.connect('', '', stompSuccessCallBack, stompFailureCallback);
}

stompConnect();

```

#まとめ

業務アプリっぽい構成で、Google Homeに発話させることができました。
意外と仕事で使うこともあるかもしれませんね。