forked from mttu-developers/konabot
wolfx api
This commit is contained in:
233
konabot/common/apis/wolfx.py
Normal file
233
konabot/common/apis/wolfx.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""
|
||||
Wolfx 防灾免费 API
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import TypeVar, cast
|
||||
import aiohttp
|
||||
from aiosignal import Signal
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
import pydantic
|
||||
|
||||
from konabot.common.appcontext import after_init
|
||||
|
||||
|
||||
class ScEewReport(BaseModel):
|
||||
"""
|
||||
四川地震局报文
|
||||
"""
|
||||
|
||||
ID: str
|
||||
"EEW 发报 ID"
|
||||
|
||||
EventID: str
|
||||
"EEW 发报事件 ID"
|
||||
|
||||
ReportTime: str
|
||||
"EEW 发报时间(UTC+8)"
|
||||
|
||||
ReportNum: int
|
||||
"EEW 发报数"
|
||||
|
||||
OriginTime: str
|
||||
"发震时间(UTC+8)"
|
||||
|
||||
HypoCenter: str
|
||||
"震源地"
|
||||
|
||||
Latitude: float
|
||||
"震源地纬度"
|
||||
|
||||
Longitude: float
|
||||
"震源地经度"
|
||||
|
||||
Magnitude: float
|
||||
"震级"
|
||||
|
||||
Depth: float | None
|
||||
"震源深度"
|
||||
|
||||
MaxIntensity: float
|
||||
"最大烈度"
|
||||
|
||||
|
||||
class CencEewReport(BaseModel):
|
||||
"""
|
||||
中国地震台网报文
|
||||
"""
|
||||
|
||||
ID: str
|
||||
"EEW 发报 ID"
|
||||
|
||||
EventID: str
|
||||
"EEW 发报事件 ID"
|
||||
|
||||
ReportTime: str
|
||||
"EEW 发报时间(UTC+8)"
|
||||
|
||||
ReportNum: int
|
||||
"EEW 发报数"
|
||||
|
||||
OriginTime: str
|
||||
"发震时间(UTC+8)"
|
||||
|
||||
HypoCenter: str
|
||||
"震源地"
|
||||
|
||||
Latitude: float
|
||||
"震源地纬度"
|
||||
|
||||
Longitude: float
|
||||
"震源地经度"
|
||||
|
||||
Magnitude: float
|
||||
"震级"
|
||||
|
||||
Depth: float | None
|
||||
"震源深度"
|
||||
|
||||
MaxIntensity: float
|
||||
"最大烈度"
|
||||
|
||||
|
||||
class WolfxWebSocket:
|
||||
def __init__(self, url: str) -> None:
|
||||
self.url = url
|
||||
self.signal: Signal[bytes] = Signal(self)
|
||||
self._running = False
|
||||
self._task: asyncio.Task | None = None
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
self._ws: aiohttp.ClientWebSocketResponse | None = None
|
||||
|
||||
@property
|
||||
def session(self) -> aiohttp.ClientSession: # pragma: no cover
|
||||
assert self._session is not None
|
||||
return self._session
|
||||
|
||||
async def start(self): # pragma: no cover
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
self._session = aiohttp.ClientSession()
|
||||
self._task = asyncio.create_task(self._run())
|
||||
self.signal.freeze()
|
||||
|
||||
async def stop(self): # pragma: no cover
|
||||
self._running = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
if self._session:
|
||||
await self._session.close()
|
||||
|
||||
async def _run(self): # pragma: no cover
|
||||
retry_delay = 1
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
async with self.session.ws_connect(self.url) as ws:
|
||||
self._ws = ws
|
||||
logger.info(f"Wolfx API 服务连接上了 {self.url} 的 WebSocket")
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
await self.handle(cast(str, msg.data).encode())
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await self.handle(cast(bytes, msg.data))
|
||||
elif msg.type == aiohttp.WSMsgType.CLOSED:
|
||||
break
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
break
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
logger.warning("连接 WebSocket 时发生错误")
|
||||
logger.exception(e)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error("Wolfx API 发生未知错误")
|
||||
logger.exception(e)
|
||||
self._ws = None
|
||||
|
||||
if self._running:
|
||||
logger.info(f"Wolfx API 准备断线重连 {self.url}")
|
||||
await asyncio.sleep(retry_delay)
|
||||
retry_delay = min(retry_delay * 2, 60)
|
||||
|
||||
async def handle(self, data: bytes):
|
||||
try:
|
||||
obj = json.loads(data)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning("解析 Wolfs API 时出错")
|
||||
logger.exception(e)
|
||||
return
|
||||
|
||||
if obj.get("type") == "heartbeat" or obj.get("type") == "pong":
|
||||
logger.debug(f"Wolfx API 收到了来自 {self.url} 的心跳: {obj}")
|
||||
else:
|
||||
await self.signal.send(data)
|
||||
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class WolfxAPIService:
|
||||
sc_eew: Signal[ScEewReport]
|
||||
"四川地震局地震速报"
|
||||
|
||||
cenc_eew: Signal[CencEewReport]
|
||||
"中国地震台网地震速报"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sc_eew = Signal(self)
|
||||
self._sc_eew_ws = WolfxWebSocket("wss://ws-api.wolfx.jp/sc_eew")
|
||||
WolfxAPIService.bind(self.sc_eew, self._sc_eew_ws, ScEewReport)
|
||||
|
||||
self.cenc_eew = Signal(self)
|
||||
self._cenc_eew_ws = WolfxWebSocket("wss://ws-api.wolfx.jp/cenc_eew")
|
||||
WolfxAPIService.bind(self.cenc_eew, self._cenc_eew_ws, CencEewReport)
|
||||
|
||||
@staticmethod
|
||||
def bind(signal: Signal[T], ws: WolfxWebSocket, t: type[T]):
|
||||
@ws.signal.append
|
||||
async def _(data: bytes):
|
||||
try:
|
||||
obj = t.model_validate_json(data)
|
||||
logger.info(f"接收到来自 Wolfx API 的信息:{data}")
|
||||
await signal.send(obj)
|
||||
except pydantic.ValidationError as e:
|
||||
logger.warning(f"解析 Wolfx API 时出错 URL={ws.url}")
|
||||
logger.error(e)
|
||||
|
||||
async def start(self): # pragma: no cover
|
||||
self.cenc_eew.freeze()
|
||||
self.sc_eew.freeze()
|
||||
async with asyncio.TaskGroup() as task_group:
|
||||
task_group.create_task(self._cenc_eew_ws.start())
|
||||
task_group.create_task(self._sc_eew_ws.start())
|
||||
|
||||
async def stop(self): # pragma: no cover
|
||||
async with asyncio.TaskGroup() as task_group:
|
||||
task_group.create_task(self._cenc_eew_ws.stop())
|
||||
task_group.create_task(self._sc_eew_ws.stop())
|
||||
|
||||
|
||||
wolfx_api = WolfxAPIService()
|
||||
|
||||
|
||||
@after_init
|
||||
def init(): # pragma: no cover
|
||||
import nonebot
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await wolfx_api.start()
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _():
|
||||
await wolfx_api.stop()
|
||||
15
konabot/common/appcontext.py
Normal file
15
konabot/common/appcontext.py
Normal file
@ -0,0 +1,15 @@
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
AFTER_INIT_FUNCTION = Callable[[], Any]
|
||||
|
||||
_after_init_functions: list[AFTER_INIT_FUNCTION] = []
|
||||
|
||||
|
||||
def after_init(func: AFTER_INIT_FUNCTION):
|
||||
_after_init_functions.append(func)
|
||||
|
||||
|
||||
def run_afterinit_functions(): # pragma: no cover
|
||||
for f in _after_init_functions:
|
||||
f()
|
||||
@ -4,6 +4,7 @@ from nonebot.adapters import Event
|
||||
from nonebot.params import Depends
|
||||
from nonebot.rule import Rule
|
||||
|
||||
from konabot.common.appcontext import after_init
|
||||
from konabot.common.database import DatabaseManager
|
||||
from konabot.common.pager import PagerQuery
|
||||
from konabot.common.path import DATA_PATH
|
||||
@ -73,6 +74,7 @@ def perm_manager(_db: DatabaseManager | None = None) -> PermManager: # pragma:
|
||||
return PermManager(_db)
|
||||
|
||||
|
||||
@after_init
|
||||
def create_startup(): # pragma: no cover
|
||||
from konabot.common.nb.is_admin import cfg
|
||||
|
||||
|
||||
82
konabot/plugins/wolfx_eew.py
Normal file
82
konabot/plugins/wolfx_eew.py
Normal file
@ -0,0 +1,82 @@
|
||||
import datetime
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from konabot.common.apis.wolfx import CencEewReport, wolfx_api
|
||||
from konabot.common.subscribe import PosterInfo, broadcast, register_poster_info
|
||||
|
||||
|
||||
provinces_short = [
|
||||
"北京",
|
||||
"天津",
|
||||
"河北",
|
||||
"山西",
|
||||
"内蒙古",
|
||||
"辽宁",
|
||||
"吉林",
|
||||
"黑龙江",
|
||||
"上海",
|
||||
"江苏",
|
||||
"浙江",
|
||||
"安徽",
|
||||
"福建",
|
||||
"江西",
|
||||
"山东",
|
||||
"河南",
|
||||
"湖北",
|
||||
"湖南",
|
||||
"广东",
|
||||
"广西",
|
||||
"海南",
|
||||
"重庆",
|
||||
"四川",
|
||||
"贵州",
|
||||
"云南",
|
||||
"西藏",
|
||||
"陕西",
|
||||
"甘肃",
|
||||
"青海",
|
||||
"宁夏",
|
||||
"新疆",
|
||||
"香港",
|
||||
"澳门",
|
||||
"台湾",
|
||||
]
|
||||
|
||||
|
||||
register_poster_info(
|
||||
"中国地震台网地震速报",
|
||||
PosterInfo(
|
||||
aliases={
|
||||
"地震速报",
|
||||
"地震预警",
|
||||
},
|
||||
description="来自中国地震台网的地震速报",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@wolfx_api.cenc_eew.append
|
||||
async def broadcast_eew(report: CencEewReport):
|
||||
is_cn = any(report.HypoCenter.startswith(prefix) for prefix in provinces_short)
|
||||
if (is_cn and report.Magnitude >= 4.2) or ((not is_cn) and report.Magnitude >= 7.0):
|
||||
# 这是中国地震台网网站上,会默认展示的地震信息的等级
|
||||
origin_time_dt = datetime.datetime.strptime(
|
||||
report.OriginTime, "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
origin_time_str = (
|
||||
f"{origin_time_dt.month}月"
|
||||
f"{origin_time_dt.day}日"
|
||||
f"{origin_time_dt.hour}时"
|
||||
f"{origin_time_dt.minute}分"
|
||||
)
|
||||
|
||||
eid_in_link = report.EventID.split(".")[0]
|
||||
link = f"https://www.cenc.ac.cn/earthquake-manage-publish-web/product-list/{eid_in_link}/summarize"
|
||||
|
||||
msg = UniMessage.text(
|
||||
"据中国地震台网中心 (https://www.cenc.ac.cn/) 报道,"
|
||||
f"北京时间{origin_time_str},"
|
||||
f"{report.HypoCenter}发生{report.Magnitude:.1f}级地震。"
|
||||
f"震源位于 {report.Longitude}° {report.Latitude}°,深度 {report.Depth}km。\n\n"
|
||||
f"详细信息请见 {link}"
|
||||
)
|
||||
await broadcast("中国地震台网地震速报", msg)
|
||||
Reference in New Issue
Block a user