気象庁サイトの注警報情報を取得し、 Ulanzi TC001 で表示させる

## 気象庁サイトの注警報 JSON ファイル
これまで、 [気象庁予報の JSON ファイルを Node-Red で Awtrix 用に変換してみる](https://qiita.com/harycurl/items/649c964a401d98cf8aa9) 、 [気象庁のアメダス情報を取得し、 PHP で1地点だけ情報を抽出する](https://qiita.com/harycurl/items/b3b3b53aef8c26573e0b) の記事で、気象庁サイトの情報を TC001 に表示させる方法を紹介してきた。
今回は第3弾。気象庁サイトの注警報ページの情報取得を試みる。


気象庁サイトの注警報ページでも何種類かの JSON ファイルが読み込まれる。

https://www.jma.go.jp/bosai/warning/#area_type=offices&area_code=200000&lang=ja

そのうち、 `https://www.jma.go.jp/bosai/warning/data/warning/地域コード.json` が注警報情報の入った JSON になる。
およその構造は以下の通り。
![jma_warn1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/90407403-2a06-abd2-2c15-2b372e3e8ccd.png)


`["areaTypes"]["0"]` 以下には一次細分区域ごとの注警報情報が、 `["areaTypes"]["1"]` 以下には市区町村単位（一部はさらに分割）の注警報情報が入っている。 
`["areas"]` `[連番]` とたどると、 `["code"]` （市区町村コード） と `["warnings"]` に分かれており、  `["warnings"]` の下は発表中の注警報の数の分だけ `[連番]` で注警報コードと状態が収められている。

そしてやっかいなことに、注警報コードを取り出すだけではだめで、状態が `解除` の時はその注警報コードを無視しないといけない。

## JavaScript で注警報情報を取得する
JSON上最初に出てくる市区町村（長野県の場合は長野市）の注警報を取得するスクリプトとして、このようなものを作成した。
```javascript: parser A
var warn_array = msg.payload;
var area_num = 0;
var warn_code = warn_array.areaTypes[1].areas[area_num].warnings;
var code = [];
var status = [];
var warn = [];

function warn_exist() {
    for (var i = 0; i < warn_code.length; i++) {
        if (warn_code[i].status !== "解除") {
            code.push(warn_code[i].code);
        }

    }

    if (code.find(value => value.match(/^3/))) {
        warn = "1";
        return;
    } else if (code.find(value => value.match(/^0/))) {
        warn = "2";
        return;
    } else if (code.find(value => value.match(/^1/))) {
        warn = "3";
        return;
    } else if (code.find(value => value.match(/^2/))) {
        warn = "3";
        return;
    } else {
        warn = "0";
        return;
    }
}

var key_num = Object.keys(warn_code[0]).length;
if (key_num > 1) {
    warn_exist();
} else {
    warn = "0";
}

msg.payload = warn;
return msg;
```
流れとしては
- 最初の連想配列を読み取り、 key が1つ（code がなく status のみ）なら無条件で注警報無し
- 連想配列を順に読み取り、 status が `解除` のもの以外を code に追加

そして code に入っている注警報コードに従って判定していく。
注警報コードは以下のページの 15 報種別コード に書かれている通り。

https://doc01.pf.iij-engineering.co.jp/pub/sdkdoc/v1/ja_JP/restapi/rest_jmaweather_ver1.html

- 先頭が3：特別警報
- 先頭が0：警報
- 先頭が1か2：注意報
- いずれも無い：注警報無し

と上から順に判定し（抽出後に配列内の要素数を `count` で数え、0より大きい=要素ありと判定）、1：特別警報、2：警報、3：注意報、0：注警報無し を返すようにした。
注警報の種類を細かく取得することも可能だが、煩雑になるため上位種別のみの表示とする。

## Node-RED でアプリとして TC001 に送信する
種別コードに変換できれば、あとは種別に合わせた JSON を生成できれば mqtt out で TC001 に情報を送ることができる。
![jma_warn4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/3f67206c-1c6a-f2bf-8866-bfca72e486e8.png)
![jma_warn5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/a2dd3336-1972-f09c-5fb8-afe11d887f34.png)
custom 配下の MQTT トピック指定はアプリ方式で、自動めくりの1ページ分となる。
注警報発表中はページが作られ、注警報が解除された時点で空メッセージをトピックに送るとアプリごと削除され表示されなくなる。

## 前回の注警報発表情報を保存しておき、相違があったら notify で表示
続いて、注警報発表・解除時に notify 機能を使って通知する方法を考える。
方略としては前回の注警報状況をファイルに保存しておき、現在の注警報を取得後に比較、違いがあればメッセージを送り出すという流れになる。
が、複数のデータを Node-RED のフローで入力する方法がわからずにいた。

解決の鍵はコンテキストにあった。

https://nodered.jp/docs/user-guide/context

この公式ドキュメントを参考に、 change フローを用いていく。
注警報状況の保存は change で flow.warn_code ← msg.payload

呼び出しは逆に change で msg.old_data ← flow.warn_code
ここで、別途 Web から取得した msg.payload の情報をこのノードに流しても、呼び出した変数情報は保存される。

この2つを使って function ノードで処理をさせ、両者が異なる場合のみメッセージを作成、 mqtt out ノードで awtirx/notify へメッセージを送信すればよい（空のメッセージの場合は何も起きない）。

ノード図はこのようになる。
![jma_warn6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/8818ccd5-8f9e-0f5b-ddca-1614044d650b.png)


メッセージの色・文字列はアプリと同様だが、
- `"blinkText": 1000` で1秒点滅（繰り返し）
- 特別警報・警報時は `"hold":true` として、本体ボタンが押されない限り表示し続ける設定
- 特別警報・警報時は `"sound":"siren"` も追加、 MELODIES フォルダにある siren.txt の rtttl 音を流す（[Rtttl Buzzer - ESPHome](https://esphome.io/components/rtttl.html) にある siren の rtttl を利用した） 
- 注意報と解除時は hold しないかわりに30秒表示

とした。
```javascript: parser 2
var new_code = msg.payload;
var old_code = msg.old_code;

if (new_code != old_code) {
    if (new_code == 1) {
        msg.payload = {"text": "SP WARN", "background": "#ff00ff", "blinkText": 1000, "sound": "siren", "hold": true};
    } else if (new_code == 2) {
        msg.payload = {"text": "WARNING", "background": "#ff0000", "blinkText": 1000, "sound": "siren", "hold": true};
    } else if (new_code == 3) {
        msg.payload = {"text": "Advisory", "color": "#ffff00", "blinkText": 1000, "duration": 30};
    } else {
        msg.payload = {"text": "No Warn", "blinkText": 1000, "duration": 30};
    }
} else {
    msg.payload = "";
}

return msg;
```
警報はこのようになる
![jma_warn7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/456edaca-7912-c0fb-917c-9a56d7964699.gif)


なお、 flow.warn_code で保管した注警報情報だが、マニュアルにある通り、デフォルトではメモリのみに保存され、再起動時に値が失われる。
setting.js の contextStorage プロパティ のコメントを5行分外すことで、値は30秒ごとにファイルに保存され、再起動しても flow.warn_code の値が保持される。変更が無ければ notify にメッセージは送られない。

かくして、気象会社・報道機関にありがちなパトライトのような機能を [TC001](https://www.ulanzi.jp/products/ulanzi-pixel-smart-clock-2882) と [MeePet](https://kaigio.com/meepet/) を使って実現できた。
とはいえ気象庁の Web から情報を引っぱってきているので遅延は発生する。業務用として使用することは勧めない。

## Node-RED フロー
最後に、一連のフローを書き出した JSON ファイルを添付する。

<details><summary>jma_warn_flows.json</summary>

```json: jma_warn_flows.json
[
    {
        "id": "bac9b1afa5d97b72",
        "type": "group",
        "z": "20b92c4c3ed7c791",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "812e1827a3f14517",
            "f276606cfc676492",
            "bfad2ad9c132e2ce",
            "314bebe1cddc315a",
            "921a539198376b4a",
            "aef2171526ef1284",
            "c901c948e9b86da8",
            "9bf90dcb571c154d",
            "004a63261651dac3",
            "290d17d75ec59ed9",
            "166f088815f70fe0"
        ],
        "x": 54,
        "y": 2539,
        "w": 1112,
        "h": 242
    },
    {
        "id": "812e1827a3f14517",
        "type": "comment",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "注警報",
        "info": "http request\n200000.json ファイル名を地域コードにする\n\nparser A\narea_num JSONにおける市区町村の順番(市区町村コードではない)",
        "x": 210,
        "y": 2580,
        "wires": []
    },
    {
        "id": "f276606cfc676492",
        "type": "inject",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "props": [],
        "repeat": "60",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 150,
        "y": 2660,
        "wires": [
            [
                "bfad2ad9c132e2ce"
            ]
        ]
    },
    {
        "id": "bfad2ad9c132e2ce",
        "type": "http request",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "https://www.jma.go.jp/bosai/warning/data/warning/200000.json",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 330,
        "y": 2660,
        "wires": [
            [
                "166f088815f70fe0"
            ]
        ]
    },
    {
        "id": "314bebe1cddc315a",
        "type": "function",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "parser 1",
        "func": "var warn_code = msg.payload;\nif (warn_code == \"1\") {\n    var text = '{\"text\": \"SP WARN\", \"background\": \"#ff00ff\"}';\n} else if (warn_code == \"2\") {\n    var text = '{\"text\": \"WARNING\", \"background\": \"#ff0000\"}';\n} else if (warn_code == \"3\") {\n    var text = '{\"text\": \"Advisory\", \"color\": \"#ffff00\"}';\n} else {\n    var text = \"\";\n}\n\nmsg.payload = text;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 680,
        "y": 2660,
        "wires": [
            [
                "921a539198376b4a"
            ]
        ]
    },
    {
        "id": "921a539198376b4a",
        "type": "mqtt out",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "topic": "awtrix/custom/warning",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "346df2a95aac5785",
        "x": 870,
        "y": 2660,
        "wires": []
    },
    {
        "id": "aef2171526ef1284",
        "type": "delay",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "pauseType": "delay",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 680,
        "y": 2580,
        "wires": [
            [
                "c901c948e9b86da8"
            ]
        ]
    },
    {
        "id": "c901c948e9b86da8",
        "type": "change",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "warn_code",
        "rules": [
            {
                "t": "set",
                "p": "warn_code",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 850,
        "y": 2580,
        "wires": [
            []
        ]
    },
    {
        "id": "9bf90dcb571c154d",
        "type": "change",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "old_code",
                "pt": "msg",
                "to": "warn_code",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 710,
        "y": 2740,
        "wires": [
            [
                "004a63261651dac3"
            ]
        ]
    },
    {
        "id": "004a63261651dac3",
        "type": "function",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "parser 2",
        "func": "var new_code = msg.payload;\nvar old_code = msg.old_code;\n\nif (new_code != old_code) {\n    if (new_code == 1) {\n        msg.payload = {\"text\": \"SP WARN\", \"background\": \"#ff00ff\", \"blinkText\": 1000, \"sound\": \"siren\", \"hold\": true};\n    } else if (new_code == 2) {\n        msg.payload = {\"text\": \"WARNING\", \"background\": \"#ff0000\", \"blinkText\": 1000, \"sound\": \"siren\", \"hold\": true};\n    } else if (new_code == 3) {\n        msg.payload = {\"text\": \"Advisory\", \"color\": \"#ffff00\", \"blinkText\": 1000, \"duration\": 30};\n    } else {\n        msg.payload = {\"text\": \"No Warn\", \"blinkText\": 1000, \"duration\": 30};\n    }\n} else {\n    msg.payload = \"\";\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 900,
        "y": 2740,
        "wires": [
            [
                "290d17d75ec59ed9"
            ]
        ]
    },
    {
        "id": "290d17d75ec59ed9",
        "type": "mqtt out",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "",
        "topic": "awtrix/notify",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "346df2a95aac5785",
        "x": 1070,
        "y": 2740,
        "wires": []
    },
    {
        "id": "166f088815f70fe0",
        "type": "function",
        "z": "20b92c4c3ed7c791",
        "g": "bac9b1afa5d97b72",
        "name": "parser A",
        "func": "var warn_array = msg.payload;\nvar area_num = 0;\nvar warn_code = warn_array.areaTypes[1].areas[area_num].warnings;\nvar code = [];\nvar status = [];\nvar warn = [];\n\nfunction warn_exist() {\n    for (var i = 0; i < warn_code.length; i++) {\n        if (warn_code[i].status !== \"解除\") {\n            code.push(warn_code[i].code);\n        }\n    }\n\n    if (code.find(value => value.match(/^3/))) {\n        warn = \"1\";\n        return;\n    } else if (code.find(value => value.match(/^0/))) {\n        warn = \"2\";\n        return;\n    } else if (code.find(value => value.match(/^1/))) {\n        warn = \"3\";\n        return;\n    } else if (code.find(value => value.match(/^2/))) {\n        warn = \"3\";\n        return;\n    } else {\n        warn = \"0\";\n        return;\n    }\n}\n\nvar key_num = Object.keys(warn_code[0]).length;\nif (key_num > 1) {\n    warn_exist();\n} else {\n    warn = \"0\";\n}\n\nmsg.payload = warn;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 2660,
        "wires": [
            [
                "314bebe1cddc315a",
                "aef2171526ef1284",
                "9bf90dcb571c154d"
            ]
        ]
    },
    {
        "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>