気象庁サイトの注警報 JSON ファイル
これまで、 気象庁予報の JSON ファイルを Node-Red で Awtrix 用に変換してみる 、 気象庁のアメダス情報を取得し、 PHP で1地点だけ情報を抽出する の記事で、気象庁サイトの情報を TC001 に表示させる方法を紹介してきた。
今回は第3弾。気象庁サイトの注警報ページの情報取得を試みる。
気象庁サイトの注警報ページでも何種類かの JSON ファイルが読み込まれる。
そのうち、 https://www.jma.go.jp/bosai/warning/data/warning/地域コード.json
が注警報情報の入った JSON になる。
およその構造は以下の通り。
["areaTypes"]["0"]
以下には一次細分区域ごとの注警報情報が、 ["areaTypes"]["1"]
以下には市区町村単位(一部はさらに分割)の注警報情報が入っている。
["areas"]
[連番]
とたどると、 ["code"]
(市区町村コード) と ["warnings"]
に分かれており、 ["warnings"]
の下は発表中の注警報の数の分だけ [連番]
で注警報コードと状態が収められている。
そしてやっかいなことに、注警報コードを取り出すだけではだめで、状態が 解除
の時はその注警報コードを無視しないといけない。
JavaScript で注警報情報を取得する
JSON上最初に出てくる市区町村(長野県の場合は長野市)の注警報を取得するスクリプトとして、このようなものを作成した。
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 報種別コード に書かれている通り。
- 先頭が3:特別警報
- 先頭が0:警報
- 先頭が1か2:注意報
- いずれも無い:注警報無し
と上から順に判定し(抽出後に配列内の要素数を count
で数え、0より大きい=要素ありと判定)、1:特別警報、2:警報、3:注意報、0:注警報無し を返すようにした。
注警報の種類を細かく取得することも可能だが、煩雑になるため上位種別のみの表示とする。
Node-RED でアプリとして TC001 に送信する
種別コードに変換できれば、あとは種別に合わせた JSON を生成できれば mqtt out で TC001 に情報を送ることができる。
custom 配下の MQTT トピック指定はアプリ方式で、自動めくりの1ページ分となる。
注警報発表中はページが作られ、注警報が解除された時点で空メッセージをトピックに送るとアプリごと削除され表示されなくなる。
前回の注警報発表情報を保存しておき、相違があったら notify で表示
続いて、注警報発表・解除時に notify 機能を使って通知する方法を考える。
方略としては前回の注警報状況をファイルに保存しておき、現在の注警報を取得後に比較、違いがあればメッセージを送り出すという流れになる。
が、複数のデータを Node-RED のフローで入力する方法がわからずにいた。
解決の鍵はコンテキストにあった。
この公式ドキュメントを参考に、 change フローを用いていく。
注警報状況の保存は change で flow.warn_code ← msg.payload
呼び出しは逆に change で msg.old_data ← flow.warn_code
ここで、別途 Web から取得した msg.payload の情報をこのノードに流しても、呼び出した変数情報は保存される。
この2つを使って function ノードで処理をさせ、両者が異なる場合のみメッセージを作成、 mqtt out ノードで awtirx/notify へメッセージを送信すればよい(空のメッセージの場合は何も起きない)。
メッセージの色・文字列はアプリと同様だが、
-
"blinkText": 1000
で1秒点滅(繰り返し) - 特別警報・警報時は
"hold":true
として、本体ボタンが押されない限り表示し続ける設定 - 特別警報・警報時は
"sound":"siren"
も追加、 MELODIES フォルダにある siren.txt の rtttl 音を流す(Rtttl Buzzer - ESPHome にある siren の rtttl を利用した) - 注意報と解除時は hold しないかわりに30秒表示
とした。
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;
なお、 flow.warn_code で保管した注警報情報だが、マニュアルにある通り、デフォルトではメモリのみに保存され、再起動時に値が失われる。
setting.js の contextStorage プロパティ のコメントを5行分外すことで、値は30秒ごとにファイルに保存され、再起動しても flow.warn_code の値が保持される。変更が無ければ notify にメッセージは送られない。
かくして、気象会社・報道機関にありがちなパトライトのような機能を TC001 と MeePet を使って実現できた。
とはいえ気象庁の Web から情報を引っぱってきているので遅延は発生する。業務用として使用することは勧めない。
Node-RED フロー
最後に、一連のフローを書き出した 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": ""
}
]