timeseries.py 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. """按测点及时间窗口取原始曲线,保存为本轮可引用的可信输入。"""
  2. import asyncio
  3. import config
  4. from extensions.main_fan import gateway_queries
  5. CURVE_PATH = '/aqjk/alarmcurve/getCurveByNodeId'
  6. def _normalize(body, node_id, start, end):
  7. if not isinstance(body, dict) or body.get('success') is False or str(body.get('code')) not in {'0', '200'}:
  8. raise ValueError('曲线业务响应未成功,请检查网关及上游返回')
  9. data = body.get('data')
  10. if data == []:
  11. rows, metadata = [], []
  12. elif (isinstance(data, list) and len(data) == 2
  13. and isinstance(data[0], list) and isinstance(data[1], list)):
  14. rows, metadata = data
  15. else:
  16. raise ValueError('曲线响应应包含 [采样记录数组, 测点信息数组],不能将未知结构当成空数据')
  17. maximum = config.get_int('MAIN_FAN_SERIES_MAX_SAMPLES', 200000)
  18. if maximum <= 0 or len(rows) > maximum:
  19. raise ValueError(f'单次返回最多接受 {maximum} 条采样,请缩小查询时间范围')
  20. for item in [*rows, *metadata]:
  21. if not isinstance(item, dict) or item.get('NodeID') != node_id:
  22. raise ValueError('曲线返回的测点编号与请求不一致,不能混用其他测点')
  23. samples, times = [], []
  24. for row in rows:
  25. if 'RealData' not in row or 'DataTime' not in row:
  26. raise ValueError('曲线采样缺少 RealData 或 DataTime,请核对实际接口字段')
  27. observed = gateway_queries.parse_time(row['DataTime'])
  28. times.append(observed)
  29. samples.append({'time': observed.isoformat(), 'value': row['RealData'],
  30. 'value_label': row.get('KgDesc'), 'status_code': row.get('StateID'),
  31. 'status_label': row.get('stateName')})
  32. # 原顺序、重复、缺值和窗口外记录全部保留;只提供质量计数,不替 Agent 选择分析样本。
  33. quality = {'duplicate_time_count': len(times) - len(set(times)),
  34. 'out_of_order_count': sum(right < left for left, right in zip(times, times[1:])),
  35. 'out_of_range_count': sum(not start <= value <= end for value in times),
  36. 'null_value_count': sum(row['value'] is None for row in samples)}
  37. return {'metadata': metadata, 'samples': samples}, {
  38. 'sample_count': len(samples), 'first_time': min(times).isoformat() if times else None,
  39. 'last_time': max(times).isoformat() if times else None, 'quality': quality,
  40. 'preview': samples[:3] + (samples[-1:] if len(samples) > 3 else [])}
  41. async def query(identifier, node_id, dept_id, start_time, end_time, user, session):
  42. params, start, end = gateway_queries.curve_parameters(node_id, dept_id, start_time, end_time)
  43. workflow, request, body = await gateway_queries.fetch(identifier, user, session, CURVE_PATH, params)
  44. data, summary = await asyncio.to_thread(_normalize, body, params['nodeIds'], start, end)
  45. return await asyncio.to_thread(gateway_queries.archive, workflow, user, session, request, body,
  46. {'timezone': 'Asia/Shanghai', **data}, summary, kind='sensor_timeseries', collection='timeseries_queries',
  47. operation='查询测点历史时序', filename='samples.json')