2341 words
12 minutes
地震API及HA家居联动
2025-03-16

目前 小区广播 暂未在国内普及开来,但我们可以换个方式,用现成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/
Author
Noctiro
Published at
2025-03-16