気象庁予報の JSON ファイルを Node-Red で Awtrix 用に変換してみる

## アイコンと英数字しか表示できない環境で気象庁予報を表示するには
Ulanzi TC001 に Awtrix を入れると、 JSON 形式に変換すれば様々な情報を LED ディスプレイに表示させることができる。
ただし、 Ulanzi TC001 は 8x32 という低解像度であり、アイコンと英数字しか表示できない。
したがって、 Ulanzi TC001 に気象庁予報の JSON ファイルの内容を表示させるには、ある程度抜粋・加工する必要がある。

気象庁予報のJSONファイルには、 [気象庁予報の JSON ファイルを PHP で RSS 形式に変換してみる](https://qiita.com/harycurl/items/64b8958ce0e1914e900e) でも示したように、以下の要素が含まれている（短期予報部分）。
- 天気コード（当日、翌日、翌々日）
- 天気予報文（当日、翌日、翌々日）
- 風（当日、翌日、翌々日）
- 降水確率（予報時刻の次の時間帯～翌日24時まで、6時間毎）
- 予想気温（当日日中最高、当日最高、翌日最低、翌日最高）

この中から、
- 当日の天気アイコン、降水確率（予報時刻に近い方から2つ分）
- 翌日の天気アイコン、翌日の最低最高気温

を表示させることにした。

予想気温のデータは4つ（5時予報と11時予報）の時と2つ（17時予報）の時があるが、どちらも後ろ2つが翌日の最低最高気温となっているため、取り出す際に後ろ2つを指定すればよい。

## 天気テロップを使ってアイコン化
気象庁予報の JSON ファイルには、 `weatherCode` として3桁の天気コードが含まれている。この天気コードとはテロップ番号とも呼ばれ、テレビ等の天気マークの元ともなっている。

テロップ番号と予報との対応表はブログ記事等で公開されており

https://www.mk-mode.com/blog/2014/03/13/weather-forecast-telop-list/

テロップ番号に応じたアイコンを自動生成している人もいる

https://github.com/ciscorn/jma-weather-images

したがって、テロップ番号に対応したアイコンを用意して TC001 に入れておけばよいが、全ての番号の 8x8 アイコンを用意するのは一苦労である。

そこで簡略化するため、先頭の桁（100の位）に対応した天気マークを出すことにする。
先頭の桁が 1 だと晴れに関する予報、 2 だとくもりに関する予報、 3 だと雨に関する予報、 4 だと雨に関する予報となっているので、それぞれ sunny 、 cloudy 、 rainy 、 snowy と名前を付けたアイコンを用意し、それらが表示されるように JSON ファイルを出力するようにする。

## Node-Red にノードを配置していく
最終的にはノードをこのように配置した。右側に出ているのが実行結果である。
![nodered_weather1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/2b2a48e1-a6f5-b0b4-c365-bed12d8eb326.png)

### inject
一連のノードを実行するタイミングを指定する。
他のノード等をトリガーとはしないため、真ん中のブロックは空欄。
起動時と繰り返し部分のみ設定する。
![nodered_weather2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/eb465a4b-5f12-ff1b-3c67-b5cdd4fa9755.png)

### http request
メソッドは GET 、 URL に気象庁の JSON ファイルを指定。
出力形式を JSON オブジェクトに。
![nodered_weather3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/afbe9d56-a8ef-bad0-433c-9646896340d5.png)

### function
今日の予報部分と明日の予報部分、2つ用意する。
コードは JavaScript を用いて書く。

JSON の内容を入れた配列から要素を取り出す方法は、
PHP だと `$weather_code = $weather_array["0"]["timeSeries"]["0"]["areas"]["0"]["weatherCodes"]["0"];`
JavaScriptだと `var weather_code = weather_array[0].timeSeries[0].areas[0].weatherCodes[0];`
というように、角カッコを付ける部分・付けない部分に違いがある。

また、配列の後ろから値を取り出すには `at` を使うとよいとのこと。

https://qiita.com/ostrich-t/items/75e721c30a03b2abef0e

以上を踏まえて以下のようなスクリプトにした。

```javascript: parser 1
var weather_array = msg.payload;
var weather_code = weather_array[0].timeSeries[0].areas[0].weatherCodes[0];

var pops = weather_array[0].timeSeries[1].areas[0].pops
var pop = pops[0] + "% " + pops[1] + "%"

var wcfd = weather_code.substr(0,1)

if (wcfd == "1") {
    var icon = "sunny"
} else if (wcfd == "2") {
    var icon = "cloudy"
} else if (wcfd == "3") {
    var icon = "rainy"
} else if (wcfd == "4") {
    var icon = "snowy"
} else {
    var icon = "unknown"
}

msg.payload = {"text": pop, "icon": icon};
return msg;
```

```javascript: parser 2
var weather_array = msg.payload;
var weather_code = weather_array[0].timeSeries[0].areas[0].weatherCodes[1];

var temps = weather_array[0].timeSeries[2].areas[0].temps
var temp1 = temps.at(-2);
var temp2 = temps.at(-1);
var temp = temp1 + "/" + temp2

var wcfd = weather_code.substr(0,1)

if (wcfd == "1") {
    var icon = "sunny"
} else if (wcfd == "2") {
    var icon = "cloudy"
} else if (wcfd == "3") {
    var icon = "rainy"
} else if (wcfd == "4") {
    var icon = "snowy"
} else {
    var icon = "unknown"
}

msg.payload = {"text": temp, "icon": icon};
return msg;
```

### mqtt out
サーバとトピックだけ指定すればよい。
サーバ欄の右側の鉛筆アイコンからプロトコルやセキュリティ等の設定ができるが、 Node-Red の Aedes MQTT broker を使うため、特に変更すべき所はない。プロトコルも初期値は MQTT V3.1.1 になっている。
![nodered_weather4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/708d7968-885f-443f-5aa4-f075c50c3bda.png)


## Node-Red で読み込める JSON 形式
以下に、全てのノードを JSON 形式で書き出したものを折りたたんだ状態で掲載する。
Node-Red かつ Awtrix という環境でしか使えないかもしれないが、参考まで。
<details><summary>jma_flows.json</summary>

```javascript: jma_flows.json
[
    {
        "id": "ad4cdde3b20f84f9",
        "type": "group",
        "z": "20b92c4c3ed7c791",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "d6e00c64663ea784",
            "913d5f2896551e5f",
            "7a2b229c8f9007ac",
            "d947a11beb3f39ed",
            "42b555562d0ba066",
            "0ec9a27bebf702ff",
            "7b59770aaf95f0e3"
        ],
        "x": 54,
        "y": 439,
        "w": 812,
        "h": 222
    },
    {
        "id": "d6e00c64663ea784",
        "type": "inject",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "",
        "props": [],
        "repeat": "3600",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 150,
        "y": 560,
        "wires": [
            [
                "7a2b229c8f9007ac"
            ]
        ]
    },
    {
        "id": "913d5f2896551e5f",
        "type": "comment",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "JMA",
        "info": "httpリクエストのURLで地域コードを指定します。\n(例: 長野県 = 200000.json)\n\n地域・地点は JSON で最初に出現するものを拾う設定になっている。\n2か所目以降にしたい場合、 areas[0] の 0 の部分を変更する。\n\nawtrix/custom/weather1\n今日の天気アイコン、降水確率（予報時刻直近12時間分）\nawtrix/custom/weather2\n明日の天気アイコン、明日の最低最高気温",
        "x": 210,
        "y": 480,
        "wires": []
    },
    {
        "id": "7a2b229c8f9007ac",
        "type": "http request",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "https://www.jma.go.jp/bosai/forecast/data/forecast/200000.json",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 310,
        "y": 560,
        "wires": [
            [
                "d947a11beb3f39ed",
                "42b555562d0ba066"
            ]
        ]
    },
    {
        "id": "d947a11beb3f39ed",
        "type": "function",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "parser 1",
        "func": "var weather_array = msg.payload;\nvar weather_code = weather_array[0].timeSeries[0].areas[0].weatherCodes[0];\n\nvar pops = weather_array[0].timeSeries[1].areas[0].pops\nvar pop = pops[0] + \"% \" + pops[1] + \"%\"\n\nvar wcfd = weather_code.substr(0,1)\n\nif (wcfd == \"1\") {\n    var icon = \"sunny\"\n} else if (wcfd == \"2\") {\n    var icon = \"cloudy\"\n} else if (wcfd == \"3\") {\n    var icon = \"rainy\"\n} else if (wcfd == \"4\") {\n    var icon = \"snowy\"\n} else {\n    var icon = \"unknown\"\n}\n\nmsg.payload = {\"text\": pop, \"icon\": icon};\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 500,
        "wires": [
            [
                "0ec9a27bebf702ff"
            ]
        ]
    },
    {
        "id": "42b555562d0ba066",
        "type": "function",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "parser 2",
        "func": "var weather_array = msg.payload;\nvar weather_code = weather_array[0].timeSeries[0].areas[0].weatherCodes[1];\n\nvar temps = weather_array[0].timeSeries[2].areas[0].temps\nvar temp1 = temps.at(-2);\nvar temp2 = temps.at(-1);\nvar temp = temp1 + \"/\" + temp2\n\nvar wcfd = weather_code.substr(0,1)\n\nif (wcfd == \"1\") {\n    var icon = \"sunny\"\n} else if (wcfd == \"2\") {\n    var icon = \"cloudy\"\n} else if (wcfd == \"3\") {\n    var icon = \"rainy\"\n} else if (wcfd == \"4\") {\n    var icon = \"snowy\"\n} else {\n    var icon = \"unknown\"\n}\n\nmsg.payload = {\"text\": temp, \"icon\": icon};\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 620,
        "wires": [
            [
                "7b59770aaf95f0e3"
            ]
        ]
    },
    {
        "id": "0ec9a27bebf702ff",
        "type": "mqtt out",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "",
        "topic": "awtrix/custom/weather1",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "346df2a95aac5785",
        "x": 730,
        "y": 500,
        "wires": []
    },
    {
        "id": "7b59770aaf95f0e3",
        "type": "mqtt out",
        "z": "20b92c4c3ed7c791",
        "g": "ad4cdde3b20f84f9",
        "name": "",
        "topic": "awtrix/custom/weather2",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "346df2a95aac5785",
        "x": 730,
        "y": 620,
        "wires": []
    },
    {
        "id": "346df2a95aac5785",
        "type": "mqtt-broker",
        "name": "",
        "broker": "localhost",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]
```
</details>

## 実機表示
Ulanzi TC001 が到着したので、実機での動作確認もできた。
降水確率はスクロールしてしまうので、 `10/10%` の表示に変更した（降水確率100%以外だとスクロールせずに済む）。
アイコンはスマホの Awtrix アプリからダウンロードできるものを利用している。
![nodered_weather5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/601b7796-3168-5199-285a-adb9ad420f57.gif)
![nodered_weather6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/d4ea054e-8a07-43ed-fb01-e832fa88a4aa.gif)

## アイコンをタイル式にしてみる
テロップ番号全てのアイコンを作るのは煩雑な作業となるが、天気が把握できればよいと割り切ってタイル式のアイコンを作る手がある。
晴：<font color="#F6AA00">■</font>、曇：<font color="#808080">■</font>、雨：<font color="#4DC4FF">■</font>、雪□ の色をベースに、時々・一時・のち等を大きさや左右等で配置。
Affinity Photo で編集し、 JPEG 形式で書き出した（PNG 形式は Awtrix で使えない）。数字のみのファイル名とかち合わないよう、頭に w を付けて保存している。
![nodered_weather7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/08f0974d-657e-4e82-6334-166cbaf42c3a.png)

コードは簡素になる。

```javascript: parser 1
var weather_array = msg.payload;
var weather_code = weather_array[0].timeSeries[0].areas[1].weatherCodes[0];

var pops = weather_array[0].timeSeries[1].areas[1].pops
var pop = pops[0] + "/" + pops[1] + "%"

var icon = "w" + weather_code

msg.payload = {"text": pop, "icon": icon};
return msg;
```
実行結果はこんな感じ。
![nodered_weather8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/ddef2ca2-8cf2-9af5-134d-dffcc77568f9.png)
![nodered_weather9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/6daeefbe-3687-99b6-b97e-3b0c07b04a2d.png)