program/collector.py

329 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据采集模块 - 从组态王API获取实时数据
"""
import json
import time
import ssl
import urllib.request
import urllib.error
from datetime import datetime
from database import get_connection
def get_unit_list():
"""获取所有工作面单位列表"""
conn = get_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, unit_code, unit_name, face_id, tag_prefix, enabled
FROM mine_units
WHERE enabled = 1
ORDER BY unit_code
""")
return cursor.fetchall()
finally:
conn.close()
def get_unit_by_code(unit_code):
"""根据单位代码获取单位配置"""
conn = get_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, unit_code, unit_name, face_id, tag_prefix, enabled
FROM mine_units
WHERE unit_code = %s
""", (unit_code,))
return cursor.fetchone()
finally:
conn.close()
def get_tag_list(unit_code, device_type=None, tag_category=None):
"""获取标签列表"""
conn = get_connection()
try:
# 验证单位是否存在
cursor = conn.cursor()
cursor.execute("SELECT id FROM mine_units WHERE unit_code = %s", (unit_code,))
result = cursor.fetchone()
if not result:
return []
conditions = []
params = []
if device_type:
# 设备类型映射
device_map = {
'shearer': 'Shearer',
'afc': 'AFC',
'loa': 'LOA',
'cru': 'CRU',
'emulsion': 'Emulsion',
'loop': 'Loop',
'spray': 'Spray',
}
if device_type in device_map:
conditions.append("tag_short_name LIKE %s")
params.append(f"{device_map[device_type]}%")
if tag_category:
conditions.append("tag_category = %s")
params.append(tag_category)
where_clause = ""
if conditions:
where_clause = f"WHERE {' AND '.join(conditions)}"
sql = f"SELECT tag_short_name, tag_category, unit as tag_unit FROM tag_definitions {where_clause} ORDER BY tag_short_name"
with conn.cursor() as cursor:
cursor.execute(sql, params)
return cursor.fetchall()
finally:
conn.close()
def query_kingview_api(tag_names, api_url, user_handle):
"""调用组态王API获取实时数据"""
payload = {
"json_str": {
"user_handle": user_handle,
"tagNames": tag_names
},
"tagNames": tag_names,
"user_handle": user_handle
}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
api_url,
data=data,
headers={
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
}
)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with urllib.request.urlopen(req, context=ctx, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except urllib.error.URLError as e:
return {"error_code": -1, "message": str(e), "datas": []}
except Exception as e:
return {"error_code": -1, "message": str(e), "datas": []}
def parse_api_response(response):
"""解析API响应数据"""
results = []
if response.get('error_code') != 0:
return results
for item in response.get('datas', []):
for tag_val in item.get('TagValues', []):
if not tag_val:
continue
try:
tag_data = json.loads(tag_val) if isinstance(tag_val, str) else tag_val
results.append({
'tag_name': tag_data.get('N', ''),
'value': tag_data.get('V'),
'timestamp': tag_data.get('T'),
'quality': tag_data.get('Q', 0)
})
except (json.JSONDecodeError, KeyError):
continue
return results
def collect_data(unit_code, api_url, user_handle, batch_size=100, device_type=None, tag_category=None):
"""
采集指定单位的数据
返回: {'status': str, 'data': list, 'count': int, 'message': str}
"""
# 获取单位配置
unit = get_unit_by_code(unit_code)
if not unit:
return {'status': 'error', 'data': [], 'count': 0, 'message': f'未找到单位: {unit_code}'}
# 获取标签列表
tags = get_tag_list(unit_code, device_type=device_type, tag_category=tag_category)
if not tags:
return {'status': 'error', 'data': [], 'count': 0, 'message': '没有找到标签定义'}
# 构建完整标签名
prefix = unit['tag_prefix']
full_tag_names = [f"{prefix}{tag['tag_short_name']}" for tag in tags]
# 分批采集
all_data = []
total_batches = (len(full_tag_names) + batch_size - 1) // batch_size
for i in range(0, len(full_tag_names), batch_size):
batch = full_tag_names[i:i+batch_size]
batch_num = i // batch_size + 1
response = query_kingview_api(batch, api_url, user_handle)
data = parse_api_response(response)
all_data.extend(data)
# 批次间短暂延迟
if batch_num < total_batches:
time.sleep(0.1)
# 保存到数据库
saved = save_to_database(unit['id'], all_data, prefix)
return {
'status': 'success',
'unit_code': unit_code,
'unit_name': unit['unit_name'],
'tag_count': len(tags),
'data_count': len(all_data),
'saved': saved,
'message': f'采集完成,共{len(all_data)}条数据'
}
def save_to_database(unit_id, data, prefix):
"""保存采集数据到数据库"""
if not data:
return 0
conn = get_connection()
try:
with conn.cursor() as cursor:
# 创建快照
cursor.execute("""
INSERT INTO data_snapshots (unit_id, snapshot_time, tag_count, status)
VALUES (%s, NOW(), %s, 'success')
""", (unit_id, len(data)))
snapshot_id = cursor.lastrowid
# 批量插入实时数据
insert_sql = """
INSERT INTO data_realtime
(snapshot_id, unit_id, tag_short_name, tag_full_name, tag_value, tag_value_bool, quality, sample_time, category)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
values = []
for item in data:
full_name = item['tag_name']
# 提取短名称
short_name = full_name
if full_name.startswith(prefix):
short_name = full_name[len(prefix):]
val = item['value']
if val is None:
val_float = None
val_bool = None
elif isinstance(val, bool):
val_float = None
val_bool = 1 if val else 0
elif isinstance(val, (int, float)):
val_float = float(val)
val_bool = None
else:
try:
val_float = float(val)
val_bool = None
except (ValueError, TypeError):
val_float = None
val_bool = None
sample_time = item.get('timestamp') or datetime.now().strftime('%Y-%m-%d %H:%M:%S')
quality = item.get('quality', 0)
values.append((snapshot_id, unit_id, short_name, full_name, val_float, val_bool, quality, sample_time, ''))
# 批量插入
if values:
cursor.executemany(insert_sql, values)
conn.commit()
return len(values)
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def get_latest_snapshot(unit_code):
"""获取最新快照数据"""
conn = get_connection()
try:
with conn.cursor() as cursor:
# 获取unit_id
cursor.execute("SELECT id FROM mine_units WHERE unit_code = %s", (unit_code,))
unit = cursor.fetchone()
if not unit:
return None
# 获取最新快照
cursor.execute("""
SELECT * FROM data_snapshots
WHERE unit_id = %s
ORDER BY id DESC LIMIT 1
""", (unit['id'],))
snapshot = cursor.fetchone()
if snapshot:
# 获取快照数据
cursor.execute("""
SELECT dr.*, td.unit as tag_unit, td.tag_category as tag_category
FROM data_realtime dr
LEFT JOIN tag_definitions td ON dr.tag_short_name = td.tag_short_name
WHERE dr.snapshot_id = %s
ORDER BY dr.tag_short_name
""", (snapshot['id'],))
snapshot['data'] = cursor.fetchall()
return snapshot
finally:
conn.close()
def get_history_data(unit_code, start_time, end_time, tag_name=None):
"""获取历史数据"""
conn = get_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT id FROM mine_units WHERE unit_code = %s", (unit_code,))
unit = cursor.fetchone()
if not unit:
return []
conditions = ["ds.unit_id = %s", "ds.snapshot_time BETWEEN %s AND %s"]
params = [unit['id'], start_time, end_time]
if tag_name:
conditions.append("dr.tag_short_name = %s")
params.append(tag_name)
sql = f"""
SELECT ds.snapshot_time, dr.tag_short_name, dr.tag_value, dr.tag_value_bool, dr.quality
FROM data_snapshots ds
JOIN data_realtime dr ON dr.snapshot_id = ds.id
WHERE {' AND '.join(conditions)}
ORDER BY ds.snapshot_time, dr.tag_short_name
"""
cursor.execute(sql, params)
return cursor.fetchall()
finally:
conn.close()