2341 words
12 minutes
地震API及HA家居联动
目前 小区广播 暂未在国内普及开来,但我们可以换个方式,用现成API来实现地震预警
API列表
IMPORTANT请注意不要过度使用!每秒不要超过两次HTTP调用
1. Wolfx
- 文档: wolfx.jp/apidoc
- 介绍: 公共整合地震数据平台,支持HTTP轮询及WebSocket长连接,数据源涵盖四川地震局、日本気象厅、福建地震局、臺灣中央氣象署、中国地震台网
2. 成都高新减灾研究所
- 用例:
- 获取预警列表:
https://mobile-new.chinaeew.cn/v1/earlywarnings?updates={count}&start_at={unixTimestamp}
- 获取指定事件:
https://mobile-new.chinaeew.cn/v1/earlywarnings/{eventID}
- 获取预警列表:
- 介绍: 众多手机厂商预警服务的幕后英雄,接口调用简单高效
3. 四川地震局
- 用例:
http://118.113.105.29:8002/api/earlywarning/jsonPageList?orderType=1&pageNo={page}&pageSize={size}&userLat={lat}&userLng={lng}
- 介绍: 主要服务四川及邻近小区域的预警信息,部分数据已在Wolfx中包含
4. 福建地震局
- 用例:
http://218.5.2.111:9088/earthquakeWarn/bulletin/list.json?pageSize={count}
- 介绍: 预警数据同样已由Wolfx涵盖
使用Node-red联动Home Assistant进行全家预警
导入后根据自己的需要来修改,转载记得注明来源
[
{
"id": "65d6e0c8e7bf6ba3",
"type": "tab",
"label": "地震预警",
"disabled": false,
"info": "",
"env": []
},
{
"id": "8d97f7155e9580c7",
"type": "group",
"z": "65d6e0c8e7bf6ba3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"websocket-in",
"json-parser",
"filter-heartbeat",
"process-earthquake",
"repeat-counter",
"delay-node",
"c04a5f3aed778a9d",
"69eed9ec853a415f",
"debug",
"40db5aa120dc5a5f",
"3d2b26f672805a21",
"cancel-button",
"handle-cancel",
"39c5048e8ad3f053",
"format_data",
"12345",
"calculate_time",
"http_request",
"2d140df7c1b6cdaa",
"de9779c4cfee6360"
],
"x": 314,
"y": 79,
"w": 1192,
"h": 482
},
{
"id": "websocket-in",
"type": "websocket in",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "WolfX EEW",
"server": "",
"client": "c6bfb47858f41b5a",
"x": 510,
"y": 320,
"wires": [
[
"json-parser"
]
]
},
{
"id": "json-parser",
"type": "json",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Parse JSON",
"property": "payload",
"action": "",
"pretty": true,
"x": 690,
"y": 320,
"wires": [
[
"filter-heartbeat"
]
]
},
{
"id": "filter-heartbeat",
"type": "switch",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Filter Heartbeat",
"property": "payload.type",
"propertyType": "msg",
"rules": [
{
"t": "neq",
"v": "heartbeat",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 860,
"y": 320,
"wires": [
[
"process-earthquake"
],
[
"40db5aa120dc5a5f"
]
]
},
{
"id": "process-earthquake",
"type": "function",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Process Earthquake",
"func": "// Your location coordinates\nconst YOUR_LATITUDE = 25.04338178; // Replace with your latitude\nconst YOUR_LONGITUDE = 118.80133734; // Replace with your longitude\n\n// Function to calculate distance between two points using Haversine formula\nfunction calculateDistance(lat1, lon1, lat2, lon2) {\n const R = 6371; // Earth's radius in km\n const dLat = (lat2 - lat1) * Math.PI / 180;\n const dLon = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +\n Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *\n Math.sin(dLon / 2) * Math.sin(dLon / 2);\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n return R * c;\n}\n\n// Function to estimate if earthquake will be felt\nfunction willBeFelt(magnitude, distance, depth) {\n const intensityAtDistance = magnitude - (1.5 * Math.log10(distance)) - (0.0075 * depth);\n return intensityAtDistance > 2.5;\n}\n\n// Process message based on type\nfunction processMessage(payload) {\n const data = {\n type: payload.type,\n magnitude: null,\n location: null,\n latitude: null,\n longitude: null,\n depth: null,\n time: null,\n intensity: null,\n reportNum: null\n };\n\n switch (payload.type) {\n case 'sc_eew':\n // 四川地震局\n data.magnitude = payload.Magunitude;\n data.location = payload.HypoCenter;\n data.latitude = payload.Latitude;\n data.longitude = payload.Longitude;\n data.depth = payload.Depth || 10;\n data.time = payload.OriginTime;\n data.reportNum = payload.ReportNum;\n break;\n\n case 'jma_eew':\n // 日本气象厅\n data.magnitude = payload.Magunitude;\n data.location = payload.Hypocenter;\n data.latitude = payload.Latitude;\n data.longitude = payload.Longitude;\n data.depth = payload.Depth;\n data.time = payload.OriginTime;\n data.intensity = payload.MaxIntensity;\n data.reportNum = payload.Serial;\n break;\n\n case 'fj_eew':\n // 福建地震局\n data.magnitude = payload.Magunitude;\n data.location = payload.HypoCenter;\n data.latitude = payload.Latitude;\n data.longitude = payload.Longitude;\n data.depth = null;\n data.time = payload.OriginTime;\n data.reportNum = payload.ReportNum;\n break;\n\n case 'cenc_eqlist':\n // 中国地震台网\n if (payload.No1) {\n data.magnitude = payload.No1.magnitude;\n data.location = payload.No1.location;\n data.latitude = payload.No1.latitude;\n data.longitude = payload.No1.longitude;\n data.depth = payload.No1.depth;\n data.time = payload.No1.time;\n data.intensity = payload.No1.intensity;\n }\n break;\n\n case 'jma_eqlist':\n // 日本气象厅地震信息\n if (payload.No1) {\n data.magnitude = payload.No1.magnitude;\n data.location = payload.No1.location;\n data.latitude = payload.No1.latitude;\n data.longitude = payload.No1.longitude;\n data.depth = payload.No1.depth;\n data.time = payload.No1.time;\n data.intensity = payload.No1.shindo;\n }\n break;\n\n case 'icl_eew':\n // 成都高新减灾研究所\n data.magnitude = payload.magnitude;\n data.location = payload.epicenter;\n data.latitude = payload.latitude;\n data.longitude = payload.longitude;\n data.depth = payload.depth;\n data.time = payload.startAt;\n data.intensity = payload.epiIntensity;\n break;\n\n default:\n // 未知源,尝试通用字段匹配\n data.magnitude = payload.Magnitude || payload.magnitude;\n data.location = payload.HypoCenter || payload.Hypocenter || payload.Location || payload.location;\n data.latitude = payload.Latitude || payload.latitude;\n data.longitude = payload.Longitude || payload.longitude;\n data.depth = payload.Depth || payload.depth || 10;\n data.time = payload.OriginTime || payload.Time || payload.time;\n data.intensity = payload.MaxIntensity || payload.Intensity || payload.intensity;\n data.reportNum = payload.ReportNum || payload.Serial || payload.reportNum;\n break;\n }\n\n return data;\n}\n\n// Main processing\nfunction processEarthquakeData(msg) {\n const data = processMessage(msg.payload);\n\n if (!data.latitude || !data.longitude || !data.magnitude) {\n console.error(\"Missing required earthquake data.\");\n return null;\n }\n\n const distance = calculateDistance(\n YOUR_LATITUDE,\n YOUR_LONGITUDE,\n data.latitude,\n data.longitude\n );\n\n const felt = willBeFelt(data.magnitude, distance, data.depth);\n\n if (felt || data.type === 'heartbeat') {\n let alertMsg = `紧急通知:发生 ${data.magnitude} 级地震!`;\n\n // 震中信息\n if (data.location) {\n alertMsg += `\\n震中:${data.location}`;\n }\n if (data.depth != null) {\n alertMsg += `,深度:${data.depth}公里`;\n }\n if (distance != null) {\n alertMsg += `,距离:${Math.round(distance)}公里`;\n }\n\n // 计算发生时间的秒数差\n if (data.time) {\n const earthquakeTime = new Date(data.time).getTime();\n const currentTime = new Date().getTime();\n const timeDifference = Math.floor((currentTime - earthquakeTime) / 1000); // 以秒为单位\n\n let timeMsg = '';\n if (timeDifference > 0) {\n timeMsg = `${timeDifference}秒前`;\n } else if (timeDifference < 0) {\n timeMsg = `${Math.abs(timeDifference)}秒后`;\n } else {\n timeMsg = '刚刚发生';\n }\n\n alertMsg += `\\n发生时间:${timeMsg}`;\n }\n\n // 最大烈度\n if (data.intensity) {\n alertMsg += `\\n最大烈度:${data.intensity}`;\n }\n\n // 信息来源\n if (data.reportNum || data.type) {\n alertMsg += \"\\n来源:\";\n switch (data.type) {\n case 'sc_eew':\n alertMsg += `四川地震局,报告号:${data.reportNum || '未知'}`;\n break;\n case 'jma_eew':\n alertMsg += `日本气象厅,报告号:${data.reportNum || '未知'}`;\n break;\n case 'fj_eew':\n alertMsg += `福建地震局,报告号:${data.reportNum || '未知'}`;\n break;\n case 'cenc_eqlist':\n alertMsg += `中国地震台网,报告号:${data.reportNum || '未知'}`;\n break;\n case 'jma_eqlist':\n alertMsg += `日本气象厅,报告号:${data.reportNum || '未知'}`;\n break;\n case 'icl_eew':\n alertMsg += `成都高新减灾研究所,报告号:${data.updates || '未知'}`;\n break;\n default:\n alertMsg += `未知来源,报告号:${data.reportNum || '未知'}`;\n break;\n }\n }\n\n // 温馨提示\n alertMsg += \"\\n请保持冷静,尽量前往安全区域。\";\n\n msg.payload = { \"message\": alertMsg };\n return msg;\n }\n\n return null;\n}\n\n// 在开始新的处理前,储存当前事件ID\nconst currentEventId = msg.payload.EventID || msg.payload.eventid || Date.now().toString();\n\n// 获取上一个事件ID\nconst lastEventId = flow.get('lastEventId');\n\n// 如果这是新的地震事件,重置计数器\nif (currentEventId !== lastEventId) {\n flow.set('lastEventId', currentEventId);\n flow.set('shouldCancel', true); // 设置取消标志\n setTimeout(() => {\n flow.set('shouldCancel', false); // 0.5秒后重置取消标志\n }, 500);\n}\n\n// 在返回前添加事件ID\nmsg.eventId = currentEventId;\nmsg.repeatCount = 0;\nreturn processEarthquakeData(msg);",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1110,
"y": 280,
"wires": [
[
"repeat-counter",
"debug",
"c04a5f3aed778a9d",
"39c5048e8ad3f053"
]
]
},
{
"id": "repeat-counter",
"type": "function",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Repeat Counter",
"func": "// 检查是否应该取消\nif (flow.get('shouldCancel')) {\n // 如果是旧消息(与当前事件ID不匹配),则取消\n const currentEventId = flow.get('lastEventId');\n if (msg.eventId !== currentEventId) {\n return null;\n }\n}\n\n// 检查重复次数\nif (msg.repeatCount < 10) {\n msg.repeatCount++;\n return msg;\n}\n\nreturn null;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1380,
"y": 280,
"wires": [
[
"delay-node"
]
]
},
{
"id": "delay-node",
"type": "delay",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "25s Delay",
"pauseType": "delay",
"timeout": "25",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 1380,
"y": 380,
"wires": [
[
"repeat-counter",
"c04a5f3aed778a9d"
]
]
},
{
"id": "c04a5f3aed778a9d",
"type": "api-call-service",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "小爱播报",
"server": "fa65d2af893104cd",
"version": 7,
"debugenabled": false,
"action": "notify.send_message",
"floorId": [],
"areaId": [],
"deviceId": [],
"entityId": [
"notify.xiaomi_cn_487420462_l15a_play_text_a_7_3",
"notify.xiaomi_cn_586094138_l05c_play_text_a_5_3"
],
"labelId": [],
"data": "{ \"message\": msg.payload.message }",
"dataType": "jsonata",
"mergeContext": "",
"mustacheAltTags": true,
"outputProperties": [],
"queue": "none",
"blockInputOverrides": false,
"domain": "notify",
"service": "send_message",
"x": 1380,
"y": 460,
"wires": [
[]
]
},
{
"id": "69eed9ec853a415f",
"type": "inject",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Test2",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "{\"type\":\"fj_eew\",\"EventID\":\"202501210001\",\"ReportTime\":\"2025-01-21T10:45:00Z\",\"ReportNum\":1,\"OriginTime\":\"2025-01-21T10:40:00Z\",\"HypoCenter\":\"测试震中2\",\"Latitude\":24.8787,\"Longitude\":118.5897,\"Magunitude\":7,\"isFinal\":false}",
"payloadType": "json",
"x": 870,
"y": 260,
"wires": [
[
"process-earthquake"
]
]
},
{
"id": "debug",
"type": "debug",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Debug Output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1380,
"y": 200,
"wires": []
},
{
"id": "40db5aa120dc5a5f",
"type": "debug",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Heartbeat",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1080,
"y": 360,
"wires": []
},
{
"id": "3d2b26f672805a21",
"type": "inject",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Test1",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "{\"type\":\"fj_eew\",\"EventID\":\"202501210001\",\"ReportTime\":\"2025-01-21T10:45:00Z\",\"ReportNum\":1,\"OriginTime\":\"2025-01-21T10:40:00Z\",\"HypoCenter\":\"测试震中1\",\"Latitude\":24.1787,\"Longitude\":118.5897,\"Magunitude\":7,\"isFinal\":false}",
"payloadType": "json",
"x": 870,
"y": 220,
"wires": [
[
"process-earthquake"
]
]
},
{
"id": "cancel-button",
"type": "inject",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "取消所有播报",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "cancel",
"payloadType": "str",
"x": 620,
"y": 420,
"wires": [
[
"handle-cancel"
]
]
},
{
"id": "handle-cancel",
"type": "function",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "Handle Cancel",
"func": "// 设置取消标志\nflow.set('shouldCancel', true);\nflow.set('lastEventId', 'cancel-' + Date.now());\n\n// 返回一个消息通知取消成功\nmsg.payload = { message: \"已取消所有播报\" };\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 420,
"wires": [
[]
]
},
{
"id": "39c5048e8ad3f053",
"type": "api-call-service",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "小爱播报 音量",
"server": "fa65d2af893104cd",
"version": 7,
"debugenabled": false,
"action": "number.set_value",
"floorId": [],
"areaId": [],
"deviceId": [],
"entityId": [
"number.xiaomi_cn_487420462_l15a_volume_p_2_1",
"number.xiaomi_cn_586094138_l05c_volume_p_2_1"
],
"labelId": [],
"data": "{\"value\": 100}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"blockInputOverrides": true,
"domain": "number",
"service": "set_value",
"x": 1400,
"y": 520,
"wires": [
[]
]
},
{
"id": "12345",
"type": "inject",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "每秒刷新",
"props": [],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": "0.1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 430,
"y": 160,
"wires": [
[
"calculate_time"
]
]
},
{
"id": "calculate_time",
"type": "function",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "计算时间戳",
"func": "var now = Date.now();\nmsg.time = now - 5000;\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 590,
"y": 160,
"wires": [
[
"http_request"
]
]
},
{
"id": "http_request",
"type": "http request",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "ICL EEW",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "https://mobile-new.chinaeew.cn/v1/earlywarnings?updates=3&start_at={{time}}",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 740,
"y": 160,
"wires": [
[
"format_data",
"de9779c4cfee6360"
]
]
},
{
"id": "format_data",
"type": "function",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "格式化数据",
"func": "var data = msg.payload.data;\nif (data.length > 0) {\n msg.type = 'icl_eew';\n msg.payload = data[0];\n return msg;\n}\nreturn null;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 910,
"y": 160,
"wires": [
[
"2d140df7c1b6cdaa"
]
]
},
{
"id": "2d140df7c1b6cdaa",
"type": "switch",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "过滤",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "nnull"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 1050,
"y": 160,
"wires": [
[
"process-earthquake"
]
]
},
{
"id": "de9779c4cfee6360",
"type": "debug",
"z": "65d6e0c8e7bf6ba3",
"g": "8d97f7155e9580c7",
"name": "ICL Debug Output",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload.data",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 930,
"y": 120,
"wires": []
},
{
"id": "c6bfb47858f41b5a",
"type": "websocket-client",
"path": "wss://ws-api.wolfx.jp/all_eew",
"tls": "",
"wholemsg": "false",
"hb": "0",
"subprotocol": "",
"headers": []
},
{
"id": "fa65d2af893104cd",
"type": "server",
"name": "Home Assistant",
"version": 5,
"addon": false,
"rejectUnauthorizedCerts": false,
"ha_boolean": "y|yes|true|on|home|open",
"connectionDelay": true,
"cacheJson": true,
"heartbeat": true,
"heartbeatInterval": 30,
"areaSelector": "friendlyName",
"deviceSelector": "friendlyName",
"entitySelector": "friendlyName",
"statusSeparator": ": ",
"statusYear": "hidden",
"statusMonth": "short",
"statusDay": "numeric",
"statusHourCycle": "default",
"statusTimeFormat": "h:m",
"enableGlobalContextStore": true
}
]
地震API及HA家居联动
https://www.noctiro.moe/posts/earthquake-api/