|
@@ -60,10 +60,14 @@ for audio_file in get_audio_files():
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+import json
|
|
|
import requests
|
|
import requests
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+import threading
|
|
|
from datetime import datetime, timedelta
|
|
from datetime import datetime, timedelta
|
|
|
|
|
+from typing import List, Optional
|
|
|
from collections import defaultdict
|
|
from collections import defaultdict
|
|
|
|
|
+from urllib import request as urllib_request, error as urllib_error
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
@@ -79,23 +83,38 @@ class PumpStateMonitor:
|
|
|
- 判断是否处于启停过渡期
|
|
- 判断是否处于启停过渡期
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
- def __init__(self, scada_url, scada_jwt, project_id,
|
|
|
|
|
- timeout=10, transition_window_minutes=15):
|
|
|
|
|
|
|
+ def __init__(self, scada_url, scada_jwt, project_id,
|
|
|
|
|
+ timeout=10, transition_window_minutes=15,
|
|
|
|
|
+ login_url="", login_username="", login_password="",
|
|
|
|
|
+ login_type="account", login_dep_id=""):
|
|
|
"""
|
|
"""
|
|
|
初始化泵状态监控器
|
|
初始化泵状态监控器
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
参数:
|
|
参数:
|
|
|
scada_url: SCADA API地址
|
|
scada_url: SCADA API地址
|
|
|
- scada_jwt: JWT认证Token
|
|
|
|
|
|
|
+ scada_jwt: JWT认证Token(静态,可为空)
|
|
|
project_id: 项目ID (用于API查询)
|
|
project_id: 项目ID (用于API查询)
|
|
|
timeout: 请求超时秒数
|
|
timeout: 请求超时秒数
|
|
|
transition_window_minutes: 启停过渡期窗口(默认 15分钟)
|
|
transition_window_minutes: 启停过渡期窗口(默认 15分钟)
|
|
|
|
|
+ login_url: 登录接口地址(启用后自动获取 JWT)
|
|
|
|
|
+ login_username: 登录用户名
|
|
|
|
|
+ login_password: 登录密码
|
|
|
|
|
+ login_type: 登录类型
|
|
|
|
|
+ login_dep_id: 部门ID
|
|
|
"""
|
|
"""
|
|
|
self.scada_url = scada_url
|
|
self.scada_url = scada_url
|
|
|
- self.scada_jwt = scada_jwt
|
|
|
|
|
|
|
+ self.scada_jwt = str(scada_jwt or "").strip()
|
|
|
self.project_id = project_id
|
|
self.project_id = project_id
|
|
|
self.timeout = timeout
|
|
self.timeout = timeout
|
|
|
self.transition_window_minutes = transition_window_minutes
|
|
self.transition_window_minutes = transition_window_minutes
|
|
|
|
|
+
|
|
|
|
|
+ # 自动登录配置
|
|
|
|
|
+ self.login_url = str(login_url or "").strip()
|
|
|
|
|
+ self.login_username = str(login_username or "").strip()
|
|
|
|
|
+ self.login_password = str(login_password or "")
|
|
|
|
|
+ self.login_type = str(login_type or "account")
|
|
|
|
|
+ self.login_dep_id = str(login_dep_id or "")
|
|
|
|
|
+ self._auth_lock = threading.Lock()
|
|
|
|
|
|
|
|
# 状态缓存: {pump_id: True/False}
|
|
# 状态缓存: {pump_id: True/False}
|
|
|
self.current_states = {}
|
|
self.current_states = {}
|
|
@@ -107,33 +126,97 @@ class PumpStateMonitor:
|
|
|
# 上次查询时间(避免频繁查询)
|
|
# 上次查询时间(避免频繁查询)
|
|
|
self.last_query_time = {}
|
|
self.last_query_time = {}
|
|
|
self.min_query_interval_seconds = 30 # 30 秒查询一次
|
|
self.min_query_interval_seconds = 30 # 30 秒查询一次
|
|
|
|
|
+
|
|
|
|
|
+ # ------------------------------------------------------------------ #
|
|
|
|
|
+ # Token 管理 #
|
|
|
|
|
+ # ------------------------------------------------------------------ #
|
|
|
|
|
+
|
|
|
|
|
+ def _can_auto_login(self) -> bool:
|
|
|
|
|
+ return bool(self.login_url and self.login_username and self.login_password)
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _extract_token(payload: dict) -> str:
|
|
|
|
|
+ if not isinstance(payload, dict):
|
|
|
|
|
+ return ""
|
|
|
|
|
+ candidates: List[object] = []
|
|
|
|
|
+ data = payload.get("data")
|
|
|
|
|
+ if isinstance(data, dict):
|
|
|
|
|
+ candidates.extend([
|
|
|
|
|
+ data.get("token"), data.get("jwt"), data.get("jwtToken"),
|
|
|
|
|
+ data.get("accessToken"), data.get("access_token"),
|
|
|
|
|
+ ])
|
|
|
|
|
+ elif isinstance(data, str):
|
|
|
|
|
+ candidates.append(data)
|
|
|
|
|
+ candidates.extend([
|
|
|
|
|
+ payload.get("token"), payload.get("jwt"), payload.get("jwtToken"),
|
|
|
|
|
+ payload.get("accessToken"), payload.get("access_token"),
|
|
|
|
|
+ ])
|
|
|
|
|
+ for value in candidates:
|
|
|
|
|
+ if isinstance(value, str) and value.strip():
|
|
|
|
|
+ return value.strip()
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
|
|
+ def _login_and_get_jwt(self) -> bool:
|
|
|
|
|
+ if not self._can_auto_login():
|
|
|
|
|
+ return False
|
|
|
|
|
+ body = json.dumps({
|
|
|
|
|
+ "UserName": self.login_username,
|
|
|
|
|
+ "Password": self.login_password,
|
|
|
|
|
+ "type": self.login_type,
|
|
|
|
|
+ "DepId": self.login_dep_id,
|
|
|
|
|
+ }).encode("utf-8")
|
|
|
|
|
+ req = urllib_request.Request(
|
|
|
|
|
+ self.login_url, data=body,
|
|
|
|
|
+ headers={"Content-Type": "application/json"}, method="POST",
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ with urllib_request.urlopen(req, timeout=self.timeout) as resp:
|
|
|
|
|
+ data = json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
+ token = self._extract_token(data)
|
|
|
|
|
+ if token:
|
|
|
|
|
+ self.scada_jwt = token
|
|
|
|
|
+ logger.info("SCADA 登录成功,JWT 已刷新")
|
|
|
|
|
+ return True
|
|
|
|
|
+ logger.warning("SCADA 登录成功但响应内无 JWT 字段")
|
|
|
|
|
+ except (urllib_error.URLError, TimeoutError, json.JSONDecodeError, Exception) as e:
|
|
|
|
|
+ logger.warning("SCADA 登录失败: %s", e)
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _ensure_jwt(self) -> bool:
|
|
|
|
|
+ if self.scada_jwt:
|
|
|
|
|
+ return True
|
|
|
|
|
+ with self._auth_lock:
|
|
|
|
|
+ if self.scada_jwt:
|
|
|
|
|
+ return True
|
|
|
|
|
+ return self._login_and_get_jwt()
|
|
|
|
|
+
|
|
|
|
|
+ def _clear_jwt(self) -> None:
|
|
|
|
|
+ with self._auth_lock:
|
|
|
|
|
+ self.scada_jwt = ""
|
|
|
|
|
|
|
|
def query_pump_status(self, point, pump_name=""):
|
|
def query_pump_status(self, point, pump_name=""):
|
|
|
"""
|
|
"""
|
|
|
查询单个泵的运行状态(使用实时数据接口)
|
|
查询单个泵的运行状态(使用实时数据接口)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
使用 current-data 接口直接获取最新一条数据,无需时间窗口查询。
|
|
使用 current-data 接口直接获取最新一条数据,无需时间窗口查询。
|
|
|
-
|
|
|
|
|
|
|
+ 支持自动登录获取 JWT,401/403 时自动刷新重试。
|
|
|
|
|
+
|
|
|
参数:
|
|
参数:
|
|
|
point: 点位标识, 如 "C.M.RO1_GYB@run"
|
|
point: 点位标识, 如 "C.M.RO1_GYB@run"
|
|
|
pump_name: 泵名称, 用于日志
|
|
pump_name: 泵名称, 用于日志
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
返回:
|
|
返回:
|
|
|
(is_running, last_change_time):
|
|
(is_running, last_change_time):
|
|
|
is_running: True=运行中, False=停机
|
|
is_running: True=运行中, False=停机
|
|
|
last_change_time: 最后一次状态变化时间 (来自 htime 字段)
|
|
last_change_time: 最后一次状态变化时间 (来自 htime 字段)
|
|
|
"""
|
|
"""
|
|
|
- headers = {
|
|
|
|
|
- "Content-Type": "application/json",
|
|
|
|
|
- "JWT-TOKEN": self.scada_jwt
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# 当前时间戳(毫秒)
|
|
# 当前时间戳(毫秒)
|
|
|
now_ms = int(datetime.now().timestamp() * 1000)
|
|
now_ms = int(datetime.now().timestamp() * 1000)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# 请求参数
|
|
# 请求参数
|
|
|
params = {"time": now_ms}
|
|
params = {"time": now_ms}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# 请求体:使用实时数据接口格式
|
|
# 请求体:使用实时数据接口格式
|
|
|
request_body = [
|
|
request_body = [
|
|
|
{
|
|
{
|
|
@@ -143,49 +226,68 @@ class PumpStateMonitor:
|
|
|
"project_id": self.project_id
|
|
"project_id": self.project_id
|
|
|
}
|
|
}
|
|
|
]
|
|
]
|
|
|
-
|
|
|
|
|
- try:
|
|
|
|
|
- response = requests.post(
|
|
|
|
|
- self.scada_url,
|
|
|
|
|
- params=params,
|
|
|
|
|
- json=request_body,
|
|
|
|
|
- headers=headers,
|
|
|
|
|
- timeout=self.timeout
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- if response.status_code == 200:
|
|
|
|
|
- data = response.json()
|
|
|
|
|
- if data.get("code") == 200 and data.get("data"):
|
|
|
|
|
- # 获取第一条数据(实时接口只返回最新一条)
|
|
|
|
|
- latest = data["data"][0]
|
|
|
|
|
- if "val" in latest:
|
|
|
|
|
- val = int(float(latest["val"]))
|
|
|
|
|
- is_running = val == 1
|
|
|
|
|
-
|
|
|
|
|
- # 解析 htime 时间字段(实时接口返回的是北京时间字符串)
|
|
|
|
|
- htime_str = latest.get("htime", "")
|
|
|
|
|
- last_change_time = None
|
|
|
|
|
- if htime_str:
|
|
|
|
|
- try:
|
|
|
|
|
- # 直接解析,不做时区转换(按北京时间处理)
|
|
|
|
|
- last_change_time = datetime.strptime(htime_str, "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
- except ValueError:
|
|
|
|
|
- logger.warning(f"无法解析 htime: {htime_str}")
|
|
|
|
|
-
|
|
|
|
|
- logger.debug(f"泵状态查询: {pump_name or point} = {'运行' if is_running else '停机'} (变化时间={htime_str})")
|
|
|
|
|
- return is_running, last_change_time
|
|
|
|
|
-
|
|
|
|
|
- # 接口返回成功但无数据
|
|
|
|
|
- logger.warning(f"泵状态查询无数据: {pump_name or point}")
|
|
|
|
|
- else:
|
|
|
|
|
- logger.warning(f"泵状态查询HTTP错误: {pump_name or point} | status={response.status_code}")
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning(f"泵状态查询失败: {pump_name or point} | {e}")
|
|
|
|
|
-
|
|
|
|
|
- # 查询失败时默认返回停机状态
|
|
|
|
|
- logger.warning(f"泵状态查询: {pump_name or point} | 查询失败,默认视为停机")
|
|
|
|
|
- return False, None
|
|
|
|
|
|
|
+
|
|
|
|
|
+ for attempt in range(2):
|
|
|
|
|
+ if not self._ensure_jwt():
|
|
|
|
|
+ logger.warning("泵状态查询失败: JWT 不可用")
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ "JWT-TOKEN": self.scada_jwt
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = requests.post(
|
|
|
|
|
+ self.scada_url,
|
|
|
|
|
+ params=params,
|
|
|
|
|
+ json=request_body,
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=self.timeout
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ data = response.json()
|
|
|
|
|
+ code = data.get("code")
|
|
|
|
|
+
|
|
|
|
|
+ if code in (401, 403) and self._can_auto_login() and attempt == 0:
|
|
|
|
|
+ self._clear_jwt()
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if code == 200 and data.get("data"):
|
|
|
|
|
+ latest = data["data"][0]
|
|
|
|
|
+ if "val" in latest:
|
|
|
|
|
+ val = int(float(latest["val"]))
|
|
|
|
|
+ is_running = val == 1
|
|
|
|
|
+
|
|
|
|
|
+ htime_str = latest.get("htime", "")
|
|
|
|
|
+ last_change_time = None
|
|
|
|
|
+ if htime_str:
|
|
|
|
|
+ try:
|
|
|
|
|
+ last_change_time = datetime.strptime(htime_str, "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
+ except ValueError:
|
|
|
|
|
+ logger.warning(f"无法解析 htime: {htime_str}")
|
|
|
|
|
+
|
|
|
|
|
+ logger.debug(f"泵状态查询: {pump_name or point} = {'运行' if is_running else '停机'} (变化时间={htime_str})")
|
|
|
|
|
+ return is_running, last_change_time
|
|
|
|
|
+
|
|
|
|
|
+ logger.warning(f"泵状态查询无数据: {pump_name or point}")
|
|
|
|
|
+ elif response.status_code in (401, 403) and self._can_auto_login() and attempt == 0:
|
|
|
|
|
+ self._clear_jwt()
|
|
|
|
|
+ if self._ensure_jwt():
|
|
|
|
|
+ continue
|
|
|
|
|
+ break
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"泵状态查询HTTP错误: {pump_name or point} | status={response.status_code}")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(f"泵状态查询失败: {pump_name or point} | {e}")
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ logger.warning(f"泵状态查询: {pump_name or point} | 查询失败,保持上次状态")
|
|
|
|
|
+ # 查询失败时返回上次已知状态,避免网络抖动误判停机触发15分钟过渡期
|
|
|
|
|
+ last_state = self.current_states.get(pump_id, True)
|
|
|
|
|
+ return last_state, self.state_change_time.get(pump_id)
|
|
|
|
|
|
|
|
def update_pump_state(self, pump_id, point, pump_name=""):
|
|
def update_pump_state(self, pump_id, point, pump_name=""):
|
|
|
"""
|
|
"""
|