enebular-agentで表示端末を作る

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/5da0957a-62f5-6b3b-1be0-39ba93bfdd93.png)

enebular-agentを使って情報を表示するための端末を作ってみたいと思って、ちょっと試行錯誤しながら作ってみました。
enebular-agentはRaspberry Piに対応しているので、Raspberry PiとRaspberry Piの上に搭載できるHDMI入力の液晶モジュールを使って、表示端末を作ってみたいと思います。
今回、表示する内容は現在の天気としました。

# 準備するもの
* Raspberry Pi 3B+
    * microSDカード（[Raspbian Buster base](downloads.raspberrypi.org/raspbian/images/raspbian-2019-09-30/) + [enebular-agent v2.15.2](https://docs.enebular.com/ja/Releases/enebular-agent/2.15.2.html) ）
        * enebular-agentのインストール方法は[enebular-agentのドキュメント](https://docs.enebular.com/ja/EnebularAgent/Installation.html)を参照ください
    * 電源
* [3.5インチ モバイルモニター Raspberry Pi用](https://www.amazon.co.jp/gp/product/B078GC8132/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&psc=1) ※他のディスプレイでも可

# 表示端末としての設定
enebular-agentは、バックグランドとして動作するアプリケーションのため、ウィンドウを持ちません。そのため、情報を表示するためにdashboardノードを使用します。
dashboardノードの出力はブラウザで表示できるので、フローの起動時にブラウザをフルスクリーン表示するようにすればディスプレイでフローで作った情報を確認できます。
また、Raspberry Piの初期設定状態だとしばらくすると画面がスリープしてしまうので、スリープしないようにする設定もフローから行います。

## dashboardノード
dashboardノードはフローに配置するとブラウザ上にUIを作れるノードです。例えば以下のようになります。

* フロー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/25bb4c82-2458-8db4-e9fc-31e6becdf7b5.png)

* ブラウザ上の表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/8b298a9c-318f-2cb9-dcfe-51948a4e6ed5.png)

enebularではdashboardノードが標準ノードとして組み込まれているのでノードを追加する必要はありません。

## ブラウザをフルスクリーンで起動
ブラウザはRaspberry Pi標準のChromiumをコマンドで起動します。コマンドの実行にはexecノードを使用します。
フロー上では、ブラウザを表示するディスプレイを指定する必要があるので、その指定とフルスクリーン表示するためのオプション、URL（dashboardノード出力先のURL）を引数とし、以下のコマンドになります。

```
DISPLAY=:0 chromium-browser --noerrdialogs --kiosk --incognito http://localhost:1880/ui
```

これをexecノードのコマンド欄に設定します。

## スリープしないようにする
ディスプレイをスリープしないようにするには以下のコマンドを実行します。

```
xset -dpms
xset s off
xset s noblank
```

これもexecノードのコマンド欄にディスプレイを指定して、連続で実行されるように設定します。

```
DISPLAY=:0 xset -dpms;DISPLAY=:0 xset s off;DISPLAY=:0 xset s noblank
```

# 天気予報取得
表示する天気予報は[OpenWeatherMap](https://openweathermap.org/)というおそらく一番有名な天気情報を取得するAPIを使って取得します。
アカウント登録（無料）を行ってAPIキーを取得すれば、指定した緯度経度の天気情報が取得できます。

東京の天気情報を取得するクエリの例です。

```
https://api.openweathermap.org/data/2.5/onecall?lat=35.681236&lon=139.767125&units=metric&lang=ja&appid={APIキー}
```

以下の様にJSON型でレスポンスが返ってきます。

```json
{
    "lat": 35.68,
    "lon": 139.77,
    "timezone": "Asia/Tokyo",
    "timezone_offset": 32400,
    "current": {
        "dt": 1607188618,
        "sunrise": 1607204202,
        "sunset": 1607239652,
        "temp": 8.56,
        "feels_like": 4.13,
        "pressure": 1024,
        "humidity": 76,
        "dew_point": 4.58,
        "uvi": 0,
        "clouds": 75,
        "visibility": 10000,
        "wind_speed": 4.6,
        "wind_deg": 360,
        "weather": [
            {
                "id": 803,
                "main": "Clouds",
                "description": "曇りがち",
                "icon": "04n"
            }
        ]
    },
    "minutely": [
      ・・・省略・・・
    ],
    "hourly": [
      ・・・省略・・・
    ],
    "daily": [
      ・・・省略・・・
    ]
}
```

# フロー
これまでの内容を組み合わせたフローが以下となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/19338d76-a9ed-5390-23ee-48a7dc4ba357.png)

```json
[{"id":"e98aabc1.0347c8","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"85407b90.7d4138","type":"inject","z":"e98aabc1.0347c8","name":"フロー起動時に1回のみ","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"2","x":190,"y":200,"wires":[["53632fbe.f963a","cc26bd5c.b8bca","f52f3df2.aee3a"]]},{"id":"53632fbe.f963a","type":"exec","z":"e98aabc1.0347c8","command":"DISPLAY=:0 chromium-browser ","addpay":false,"append":"--noerrdialogs --kiosk --incognito http://localhost:1880/ui","useSpawn":"false","timer":"","oldrc":false,"name":"","x":490,"y":220,"wires":[[],[],[]]},{"id":"b3703400.f25008","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":2,"width":0,"height":0,"name":"","label":"時刻","format":"{{msg.payload.time}}","layout":"row-left","x":830,"y":340,"wires":[]},{"id":"f52f3df2.aee3a","type":"http request","z":"e98aabc1.0347c8","name":"","method":"GET","ret":"obj","paytoqs":false,"url":"https://api.openweathermap.org/data/2.5/onecall?lat=35.681236&lon=139.767125&units=metric&lang=ja&appid={API Key}","tls":"","persist":false,"proxy":"","authType":"","x":350,"y":420,"wires":[["6753aba1.420674","e1ab93f4.ba407","3d3e17fe.542668"]]},{"id":"d12b5235.ecffb","type":"inject","z":"e98aabc1.0347c8","name":"5分周期","topic":"","payload":"","payloadType":"date","repeat":"300","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":420,"wires":[["f52f3df2.aee3a"]]},{"id":"6753aba1.420674","type":"function","z":"e98aabc1.0347c8","name":"現在の天気情報を分解","func":"var json = msg.payload;\nconst dt = new Date(json.current.dt * 1000 + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));\nvar text;\n\nmsg.payload.date = dt.getFullYear() + '年'\n        + (dt.getMonth() + 1) + '月'\n        + dt.getDate() + '日 ';\n\nmsg.payload.time = dt.getHours() + '時'\n        + dt.getMinutes() + '分'\n        + dt.getSeconds() + '秒\\n';\n\n\nmsg.payload.temp = json.current.temp + '℃';\nmsg.payload.humidity = json.current.humidity + '％';\nmsg.payload.pressure = json.current.pressure + 'hPa';\nmsg.payload.weather = json.current.weather[0].description;\nmsg.payload.rain = 0;\nif('rain' in json.current) {\n    if('1h' in json.current.rain){\n        msg.payload.rain = json.current.rain[\"1h\"];\n    }\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":580,"y":420,"wires":[["b3703400.f25008","a843f73a.c73d88","79b30ea7.fdb4","e0459366.3c27f","8ba22476.d72fc8","5458a957.1e2b08","ae888926.2b90e8"]]},{"id":"a843f73a.c73d88","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":3,"width":0,"height":0,"name":"","label":"気温","format":"{{msg.payload.temp}}","layout":"row-left","x":830,"y":380,"wires":[]},{"id":"79b30ea7.fdb4","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":4,"width":0,"height":0,"name":"","label":"湿度","format":"{{msg.payload.humidity}}","layout":"row-left","x":830,"y":420,"wires":[]},{"id":"e0459366.3c27f","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":5,"width":0,"height":0,"name":"","label":"気圧","format":"{{msg.payload.pressure}}","layout":"row-left","x":830,"y":460,"wires":[]},{"id":"8ba22476.d72fc8","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":6,"width":0,"height":0,"name":"","label":"天気","format":"{{msg.payload.weather}}","layout":"row-left","x":830,"y":500,"wires":[]},{"id":"5458a957.1e2b08","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":7,"width":0,"height":0,"name":"","label":"降水量(1時間当たり)","format":"{{msg.payload.rain}}","layout":"row-left","x":880,"y":540,"wires":[]},{"id":"8923cbfd.6484a8","type":"ui_chart","z":"e98aabc1.0347c8","name":"","group":"95aa5e6a.2039a","order":6,"width":"5","height":"5","label":"気温","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":830,"y":580,"wires":[[]]},{"id":"e1ab93f4.ba407","type":"function","z":"e98aabc1.0347c8","name":"現在気温を抽出","func":"var json = msg.payload;\nconst dt = new Date(json.current.dt * 1000 + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));\n\nmsg.payload = json.current.temp;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":580,"y":580,"wires":[["8923cbfd.6484a8"]]},{"id":"3d3e17fe.542668","type":"function","z":"e98aabc1.0347c8","name":"現在気圧を抽出","func":"var json = msg.payload;\nconst dt = new Date(json.current.dt * 1000 + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));\n\nmsg.payload = json.current.pressure;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":580,"y":640,"wires":[["c1abef76.8fe13"]]},{"id":"c1abef76.8fe13","type":"ui_chart","z":"e98aabc1.0347c8","name":"","group":"95aa5e6a.2039a","order":6,"width":"5","height":"5","label":"気圧","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":830,"y":640,"wires":[[]]},{"id":"ae888926.2b90e8","type":"ui_text","z":"e98aabc1.0347c8","group":"65b0e5ae.ce33dc","order":1,"width":0,"height":0,"name":"","label":"日付","format":"{{msg.payload.date}}","layout":"row-left","x":830,"y":300,"wires":[]},{"id":"cc26bd5c.b8bca","type":"exec","z":"e98aabc1.0347c8","command":"DISPLAY=:0 xset -dpms;DISPLAY=:0 xset s off;DISPLAY=:0 xset s noblank","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":630,"y":160,"wires":[[],[],[]]},{"id":"65b0e5ae.ce33dc","type":"ui_group","z":"","name":"現在の天気予報","tab":"d6259073.9e544","order":1,"disp":true,"width":"5","collapse":false},{"id":"95aa5e6a.2039a","type":"ui_group","z":"","name":"グラフ","tab":"d6259073.9e544","order":2,"disp":false,"width":"5","collapse":false},{"id":"d6259073.9e544","type":"ui_tab","z":"","name":"ホーム","icon":"dashboard","disabled":false,"hidden":false}]
```
※使用する場合はhttp requestノードのURLで{API key}の部分を書き換えてください。

このフローをenebular-agentにデプロイすれば現在の天気情報＋気温と気圧の変化グラフを表示するための端末ができあがります。

# さらに
enebular-agentは普通にインストールするとRaspberry Piが起動するだけで、裏で起動し、フローでブラウザが起動するので、電源を入れるだけで表示端末として機能するのですが、表示端末として使用する場合はマウスカーソルが邪魔になります。
マウスカーソルを非表示にするためには以下のコマンドで`unclutter`をインストールし、再起動すれば非表示になります。

```
> sudo apt-get install unclutter
```

ただ、今回はこのインストールをenebularの[ファイルデプロイ機能](docs.enebular.com/ja/Files/Introduction.html)を使ってやってみました。

ファイルデプロイ機能はファイルをenebular-agentにデプロイできる機能です。デプロイする際にファイルを実行できるので、シェルスクリプトのファイルを用意して、インストールするコマンドを実行するようにします。シェルスクリプトのファイルの内容を以下のようにしました。

```mouseCursorDisable.sh 
#!/bin/bash

sudo apt-get install unclutter
sudo reboot
```

このファイルをファイルアセットとしてenebularに登録します。（`Excute On Deploy`をONするとデプロイ時に実行されます。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/72e47c2d-66ee-776e-ce29-07a583cfb0cf.png)


登録が完了したらenebular-agentにデプロイするとインストール後、自動的に再起動してマウスカーソルが表示となりました。

端末を複数用意する場合など、１台１台インストールするのが面倒な場合はスクリプトを作って、enebularからファイルデプロイすると楽です。

<!--
* 解像度を変更
sudo leafpad /boot/config.txt
```
＃hdmi_group=1
＃hdmi_mode=1
```
を変更
-->