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://fuwari.vercel.app/posts/earthquake-api/