feat(config): 添加配置管理和MQTT模拟服务功能
- 实现了应用配置的数据类结构(MqttConfig, TmsConfig, AppConfig) - 创建了配置加载和解析功能,支持从YAML文件读取配置 - 添加了TDengine数据库配置和连接池管理 - 实现了MQTT客户端依赖注入和服务构建 - 创建了钻孔实时数据的ORM映射和SQL构建功能 - 实现了TDengine Writer用于数据写入超级表 - 添加了MQTT模拟服务,支持发布、订阅和数据转发功能 - 创建了随机数据发送器用于测试 - 实现了消息持久化到本地文件功能 - 配置了数据库连接池和SQL执行功能
This commit is contained in:
14
db/__init__.py
Normal file
14
db/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from db.config import TDengineConfig, load_tdengine_config, parse_taos_url
|
||||
from db.orm import DrillingRealtimeORM
|
||||
from db.pool import TaosConnectionPool, create_taos_pool
|
||||
from db.writer import TDengineWriter
|
||||
|
||||
__all__ = [
|
||||
"DrillingRealtimeORM",
|
||||
"TDengineConfig",
|
||||
"TDengineWriter",
|
||||
"TaosConnectionPool",
|
||||
"create_taos_pool",
|
||||
"load_tdengine_config",
|
||||
"parse_taos_url",
|
||||
]
|
||||
65
db/config.py
Normal file
65
db/config.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from config import get_value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TDengineConfig:
|
||||
url: str = ""
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
database: str = ""
|
||||
stable: str = "drilling_realtime_st"
|
||||
device_code: str = "GJ-304-0088"
|
||||
pool_size: int = 2
|
||||
timeout: int = 10
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return bool(self.base_url and self.database and self.username)
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
base_url, _ = parse_taos_url(self.url)
|
||||
return base_url
|
||||
|
||||
|
||||
def parse_taos_url(jdbc_url):
|
||||
if not jdbc_url:
|
||||
return "", ""
|
||||
raw = str(jdbc_url).strip()
|
||||
if raw.lower().startswith("jdbc:taos-rs://"):
|
||||
raw = "http://" + raw[len("jdbc:TAOS-RS://") :]
|
||||
elif "://" not in raw:
|
||||
raw = "http://" + raw
|
||||
parsed = urlparse(raw)
|
||||
base_url = f"{parsed.scheme or 'http'}://{parsed.hostname or '127.0.0.1'}:{parsed.port or 6041}"
|
||||
database = (parsed.path or "").strip("/")
|
||||
return base_url, database
|
||||
|
||||
|
||||
def _resolve_raw_config(cfg_or_app):
|
||||
raw = getattr(cfg_or_app, "raw", None)
|
||||
return raw if isinstance(raw, dict) else cfg_or_app
|
||||
|
||||
|
||||
def load_tdengine_config(cfg_or_app, default_device_code="GJ-304-0088"):
|
||||
cfg = _resolve_raw_config(cfg_or_app)
|
||||
url = get_value(cfg, ("tdengine", "url"), ("tdengine-url",), default="")
|
||||
_, database_from_url = parse_taos_url(url)
|
||||
return TDengineConfig(
|
||||
url=url,
|
||||
username=get_value(cfg, ("tdengine", "username"), ("tdengine-username",), default=""),
|
||||
password=get_value(cfg, ("tdengine", "password"), ("tdengine-password",), default=""),
|
||||
database=get_value(cfg, ("tdengine", "database"), ("tdengine-database",), default=database_from_url),
|
||||
stable=get_value(cfg, ("tdengine", "stable"), ("tdengine-stable",), default="drilling_realtime_st"),
|
||||
device_code=get_value(
|
||||
cfg,
|
||||
("tdengine", "device-code"),
|
||||
("tdengine", "equipment-sn"),
|
||||
default=default_device_code,
|
||||
),
|
||||
pool_size=int(get_value(cfg, ("tdengine", "pool-size"), ("tdengine-pool-size",), default=2)),
|
||||
timeout=int(get_value(cfg, ("tdengine", "timeout"), ("tdengine-timeout",), default=10)),
|
||||
)
|
||||
122
db/orm.py
Normal file
122
db/orm.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
DB_COLUMNS = [
|
||||
"ts",
|
||||
"stknum",
|
||||
"recid",
|
||||
"seqid",
|
||||
"actual_date",
|
||||
"actual_time",
|
||||
"actcod",
|
||||
"deptbitm",
|
||||
"deptbitv",
|
||||
"deptmeas",
|
||||
"deptvert",
|
||||
"blkpos",
|
||||
"ropa",
|
||||
"hkla",
|
||||
"hklx",
|
||||
"woba",
|
||||
"wobx",
|
||||
"torqa",
|
||||
"torqx",
|
||||
"rpma",
|
||||
"sppa",
|
||||
"chkp",
|
||||
"spm1",
|
||||
"spm2",
|
||||
"spm3",
|
||||
"tvolact",
|
||||
"tvolcact",
|
||||
"mfop",
|
||||
"mfoa",
|
||||
"mfia",
|
||||
"mdoa",
|
||||
"mdia",
|
||||
"mtoa",
|
||||
"mtia",
|
||||
"mcoa",
|
||||
"mcia",
|
||||
"stkc",
|
||||
"lagstks",
|
||||
"deptretm",
|
||||
"gasa",
|
||||
"space1",
|
||||
"space2",
|
||||
"space3",
|
||||
"space4",
|
||||
"space5",
|
||||
]
|
||||
|
||||
|
||||
INT_COLUMNS = {"stknum", "recid", "seqid", "actcod", "rpma", "spm1", "spm2", "spm3", "mfop", "stkc", "lagstks"}
|
||||
|
||||
|
||||
def sanitize_identifier(value, fallback):
|
||||
cleaned = re.sub(r"[^A-Za-z0-9_]", "_", str(value or ""))
|
||||
if not cleaned:
|
||||
cleaned = fallback
|
||||
if cleaned[0].isdigit():
|
||||
cleaned = f"t_{cleaned}"
|
||||
return cleaned.lower()
|
||||
|
||||
|
||||
def sql_quote(value):
|
||||
return "'" + str(value).replace("'", "''") + "'"
|
||||
|
||||
|
||||
def to_int(value, default=0):
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def to_float(value, default=0.0):
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
class DrillingRealtimeORM:
|
||||
def __init__(self, database, stable="drilling_realtime_st", default_device_code="GJ-304-0088"):
|
||||
self.database = database
|
||||
self.stable = stable
|
||||
self.default_device_code = default_device_code or "GJ-304-0088"
|
||||
|
||||
def build_insert_sql(self, payload):
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("payload is not JSON object")
|
||||
meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else {}
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
|
||||
equipment_code = (
|
||||
str(self.default_device_code).strip()
|
||||
or str(meta.get("equipment_code", "")).strip()
|
||||
or str(meta.get("equipment_sn", "")).strip()
|
||||
or "GJ-304-0088"
|
||||
)
|
||||
table_name = sanitize_identifier(
|
||||
f"drilling_realtime_{equipment_code}",
|
||||
"drilling_realtime_default",
|
||||
)
|
||||
|
||||
values = []
|
||||
for col in DB_COLUMNS:
|
||||
if col == "ts":
|
||||
raw = data.get("ts", data.get("record_time", int(time.time() * 1000)))
|
||||
values.append(str(to_int(raw, int(time.time() * 1000))))
|
||||
elif col in INT_COLUMNS:
|
||||
values.append(str(to_int(data.get(col, 0), 0)))
|
||||
else:
|
||||
values.append(str(to_float(data.get(col, 0), 0.0)))
|
||||
|
||||
columns_sql = ", ".join([f"`{column}`" for column in DB_COLUMNS])
|
||||
values_sql = ", ".join(values)
|
||||
return (
|
||||
f"INSERT INTO `{self.database}`.`{table_name}` USING `{self.database}`.`{self.stable}` "
|
||||
f"TAGS ({sql_quote(equipment_code)}) ({columns_sql}) VALUES ({values_sql})"
|
||||
)
|
||||
60
db/pool.py
Normal file
60
db/pool.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import base64
|
||||
from queue import LifoQueue
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
class TaosConnection:
|
||||
def __init__(self, base_url, username, password, timeout=10):
|
||||
self.base_url = base_url
|
||||
self.username = username or ""
|
||||
self.password = password or ""
|
||||
self.timeout = timeout
|
||||
|
||||
def execute(self, sql):
|
||||
auth = f"{self.username}:{self.password}"
|
||||
auth_header = base64.b64encode(auth.encode("utf-8")).decode("ascii")
|
||||
req = Request(
|
||||
url=f"{self.base_url}/rest/sql",
|
||||
data=sql.encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Basic {auth_header}",
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urlopen(req, timeout=self.timeout) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"HTTP {resp.status} {body}")
|
||||
return body
|
||||
|
||||
|
||||
class TaosConnectionPool:
|
||||
def __init__(self, base_url, username, password, pool_size=2, timeout=10):
|
||||
self.base_url = base_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.pool_size = max(int(pool_size or 1), 1)
|
||||
self.timeout = timeout
|
||||
self._pool = LifoQueue(maxsize=self.pool_size)
|
||||
for _ in range(self.pool_size):
|
||||
self._pool.put(TaosConnection(base_url, username, password, timeout=timeout))
|
||||
|
||||
def execute(self, sql):
|
||||
conn = self._pool.get()
|
||||
try:
|
||||
return conn.execute(sql)
|
||||
finally:
|
||||
self._pool.put(conn)
|
||||
|
||||
|
||||
def create_taos_pool(config):
|
||||
if not config.enabled:
|
||||
return None
|
||||
return TaosConnectionPool(
|
||||
config.base_url,
|
||||
config.username,
|
||||
config.password,
|
||||
pool_size=config.pool_size,
|
||||
timeout=config.timeout,
|
||||
)
|
||||
20
db/writer.py
Normal file
20
db/writer.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from db.orm import DrillingRealtimeORM
|
||||
from db.pool import create_taos_pool
|
||||
|
||||
|
||||
class TDengineWriter:
|
||||
def __init__(self, config, pool=None, orm=None):
|
||||
self.config = config
|
||||
self.pool = pool if pool is not None else create_taos_pool(config)
|
||||
self.orm = orm if orm is not None else DrillingRealtimeORM(
|
||||
config.database,
|
||||
stable=config.stable,
|
||||
default_device_code=config.device_code,
|
||||
)
|
||||
self.enabled = bool(config.enabled and self.pool)
|
||||
|
||||
def write_payload(self, payload):
|
||||
if not self.enabled:
|
||||
return None
|
||||
sql = self.orm.build_insert_sql(payload)
|
||||
return self.pool.execute(sql)
|
||||
Reference in New Issue
Block a user