661 lines
26 KiB
Python
661 lines
26 KiB
Python
|
|
#!/usr/bin/env python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
测试部位温度记录表生成脚本
|
|||
|
|
|
|||
|
|
- 忽略传入的 experimentProcess,自行构造固定结构的数据
|
|||
|
|
- 从 InfluxDB 查询每个测试部位在各时间点的瞬时温度值
|
|||
|
|
- 输出格式与应用中的 scriptTable 占位符兼容
|
|||
|
|
- 默认把 {scriptTable1} 放在“测试部位”所在的单元格
|
|||
|
|
|
|||
|
|
环境变量:
|
|||
|
|
TABLE_TOKEN 目标占位符,默认 scriptTable1
|
|||
|
|
TABLE_START_ROW 写入起始行偏移,默认 0
|
|||
|
|
TABLE_START_COL 写入起始列偏移,默认 0
|
|||
|
|
TABLE_TIME_SLOTS 逗号分隔的时间刻度,默认 "0.5h,1h,1.5h,2h,2.5h,3h,3.5h"
|
|||
|
|
TABLE_MOTOR_SPEED 电机转速标签,默认 "980RPM"
|
|||
|
|
EXPERIMENT_START 实验开始时间(ISO 8601 格式,如 2024-01-01T10:00:00Z)
|
|||
|
|
EXPERIMENT_END 实验结束时间(ISO 8601 格式)
|
|||
|
|
INFLUX_URL InfluxDB URL
|
|||
|
|
INFLUX_ORG InfluxDB 组织
|
|||
|
|
INFLUX_TOKEN InfluxDB 访问令牌
|
|||
|
|
INFLUX_BUCKET InfluxDB bucket 名称
|
|||
|
|
INFLUX_MEASUREMENT InfluxDB measurement 名称
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from typing import Any, Dict, List, Optional
|
|||
|
|
|
|||
|
|
|
|||
|
|
LOGGER = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mask_secret(value: Optional[str]) -> str:
|
|||
|
|
if value is None:
|
|||
|
|
return "<unset>"
|
|||
|
|
value = value.strip()
|
|||
|
|
if not value:
|
|||
|
|
return "<empty>"
|
|||
|
|
if len(value) <= 4:
|
|||
|
|
return "****"
|
|||
|
|
return f"{value[:4]}****"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _log_environment_variables() -> None:
|
|||
|
|
env_keys = [
|
|||
|
|
"TABLE_TOKEN",
|
|||
|
|
"TABLE_START_ROW",
|
|||
|
|
"TABLE_START_COL",
|
|||
|
|
"TABLE_TIME_SLOTS",
|
|||
|
|
"TABLE_MOTOR_SPEED",
|
|||
|
|
"TABLE_LOG_LEVEL",
|
|||
|
|
"TABLE_LOG_FILE",
|
|||
|
|
"EXPERIMENT_START",
|
|||
|
|
"EXPERIMENT_END",
|
|||
|
|
"INFLUX_URL",
|
|||
|
|
"INFLUX_ORG",
|
|||
|
|
"INFLUX_TOKEN",
|
|||
|
|
"INFLUX_BUCKET",
|
|||
|
|
"INFLUX_MEASUREMENT",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for key in env_keys:
|
|||
|
|
raw_value = os.environ.get(key)
|
|||
|
|
if key.endswith("TOKEN") or key.endswith("PASSWORD"):
|
|||
|
|
display_value = _mask_secret(raw_value)
|
|||
|
|
else:
|
|||
|
|
display_value = raw_value if raw_value is not None else "<unset>"
|
|||
|
|
LOGGER.info("ENV %s = %s", key, display_value)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _read_all_stdin() -> str:
|
|||
|
|
try:
|
|||
|
|
if sys.stdin and not sys.stdin.closed and not sys.stdin.isatty():
|
|||
|
|
return sys.stdin.read()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_payload() -> Dict[str, Any]:
|
|||
|
|
raw = _read_all_stdin().strip()
|
|||
|
|
|
|||
|
|
if not raw and len(sys.argv) > 1:
|
|||
|
|
arg = sys.argv[1]
|
|||
|
|
if os.path.exists(arg) and os.path.isfile(arg):
|
|||
|
|
with open(arg, "r", encoding="utf-8") as fh:
|
|||
|
|
raw = fh.read()
|
|||
|
|
else:
|
|||
|
|
raw = arg
|
|||
|
|
|
|||
|
|
if not raw:
|
|||
|
|
raw = os.environ.get("EXPERIMENT_JSON", "").strip()
|
|||
|
|
|
|||
|
|
if not raw:
|
|||
|
|
raw = "{}"
|
|||
|
|
|
|||
|
|
data = json.loads(raw)
|
|||
|
|
if not isinstance(data, dict):
|
|||
|
|
raise ValueError("experiment JSON must be a dict")
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _time_slots() -> List[str]:
|
|||
|
|
raw = os.environ.get("TABLE_TIME_SLOTS", "").strip()
|
|||
|
|
if not raw:
|
|||
|
|
# 根据图片,时间刻度是:0.5h, 1h, 1.5h, 2h, 2.5h, 3h, 3.5h(7列)
|
|||
|
|
return ["0.5h", "1h", "1.5h", "2h", "2.5h", "3h", "3.5h"]
|
|||
|
|
slots = [slot.strip() for slot in raw.split(",")]
|
|||
|
|
return [slot for slot in slots if slot]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _default_sections() -> List[Dict[str, Any]]:
|
|||
|
|
# name -> rows underneath(entries)
|
|||
|
|
# 每个 entry 对应一个测试部位,需要映射到 InfluxDB 的 field 或 tag
|
|||
|
|
return [
|
|||
|
|
{"name": "主轴承", "entries": [
|
|||
|
|
{"label": "#1", "field": "主轴承#1", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#1"},
|
|||
|
|
{"label": "#2", "field": "主轴承#2", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#2"},
|
|||
|
|
{"label": "#3", "field": "主轴承#3", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#3"},
|
|||
|
|
{"label": "#4", "field": "主轴承#4", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#4"},
|
|||
|
|
{"label": "#5", "field": "主轴承#5", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#5"},
|
|||
|
|
{"label": "#6", "field": "主轴承#6", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#6"},
|
|||
|
|
]},
|
|||
|
|
{"name": "十字头", "entries": [
|
|||
|
|
{"label": "#1", "field": "十字头#1", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#1"},
|
|||
|
|
{"label": "#2", "field": "十字头#2", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#2"},
|
|||
|
|
{"label": "#3", "field": "十字头#3", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#3"},
|
|||
|
|
{"label": "#4", "field": "十字头#4", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#4"},
|
|||
|
|
{"label": "#5", "field": "十字头#5", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#5"},
|
|||
|
|
]},
|
|||
|
|
{"name": "减速箱小轴承", "entries": [
|
|||
|
|
{"label": "#1(输入法兰端)", "field": "减速箱小轴承1", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱小轴承#1"},
|
|||
|
|
{"label": "#2", "field": "减速箱小轴承#2", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱小轴承#2"},
|
|||
|
|
]},
|
|||
|
|
{"name": "减速箱大轴承", "entries": [
|
|||
|
|
{"label": "#3(大端盖端)", "field": "减速箱大轴承#3", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱大轴承#3"},
|
|||
|
|
{"label": "#4", "field": "减速箱大轴承#4", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱大轴承#4"},
|
|||
|
|
]},
|
|||
|
|
{"name": "润滑油温", "entries": [
|
|||
|
|
{"label": "", "field": "mean", "filters": {"data_type": "润滑油温"}, "result_key": "润滑油温"},
|
|||
|
|
]},
|
|||
|
|
{"name": "润滑油压", "entries": [
|
|||
|
|
{"label": "(Psi)", "field": "mean", "filters": {"data_type": "润滑油压"}, "result_key": "润滑油压"},
|
|||
|
|
]},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_time_slot(slot_str: str) -> float:
|
|||
|
|
"""解析时间刻度字符串(如 '0.5h', '1h')为小时数"""
|
|||
|
|
slot_str = slot_str.strip().lower()
|
|||
|
|
if slot_str.endswith('h'):
|
|||
|
|
try:
|
|||
|
|
return float(slot_str[:-1])
|
|||
|
|
except ValueError:
|
|||
|
|
pass
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _query_influxdb(
|
|||
|
|
field_name: str,
|
|||
|
|
start_time: datetime,
|
|||
|
|
end_time: datetime,
|
|||
|
|
influx_url: str,
|
|||
|
|
influx_org: str,
|
|||
|
|
influx_token: str,
|
|||
|
|
influx_bucket: str,
|
|||
|
|
influx_measurement: str,
|
|||
|
|
filters: Optional[Dict[str, str]] = None,
|
|||
|
|
) -> Optional[float]:
|
|||
|
|
"""查询 InfluxDB 获取指定字段在指定时间点的瞬时值"""
|
|||
|
|
try:
|
|||
|
|
from influxdb_client import InfluxDBClient
|
|||
|
|
import pandas as pd
|
|||
|
|
import warnings
|
|||
|
|
from influxdb_client.client.warnings import MissingPivotFunction
|
|||
|
|
except ImportError:
|
|||
|
|
LOGGER.warning("InfluxDB client not available, skip query for field=%s", field_name)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token)
|
|||
|
|
query_api = client.query_api()
|
|||
|
|
|
|||
|
|
# 构建 Flux 查询
|
|||
|
|
start_rfc3339 = start_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|||
|
|
stop_rfc3339 = end_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|||
|
|
|
|||
|
|
tag_filters = ""
|
|||
|
|
if filters:
|
|||
|
|
for key, value in filters.items():
|
|||
|
|
tag_filters += f'\n |> filter(fn: (r) => r["{key}"] == "{value}")'
|
|||
|
|
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Querying field=%s measurement=%s target_time=%s filters=%s",
|
|||
|
|
field_name,
|
|||
|
|
influx_measurement,
|
|||
|
|
end_time.strftime('%Y-%m-%dT%H:%M:%SZ'), # 使用end_time作为目标时间点
|
|||
|
|
filters or {},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 修改查询逻辑:查询目标时间点附近的最近一个数据点
|
|||
|
|
# 使用一个小的时间窗口(比如前后5分钟)来查找最接近的数据点
|
|||
|
|
target_time = end_time
|
|||
|
|
window_minutes = 5 # 前后5分钟的窗口
|
|||
|
|
|
|||
|
|
query_start = target_time - timedelta(minutes=window_minutes)
|
|||
|
|
query_end = target_time + timedelta(minutes=window_minutes)
|
|||
|
|
|
|||
|
|
query_start_rfc = query_start.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|||
|
|
query_end_rfc = query_end.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|||
|
|
|
|||
|
|
flux = f'''
|
|||
|
|
from(bucket: "{influx_bucket}")
|
|||
|
|
|> range(start: {query_start_rfc}, stop: {query_end_rfc})
|
|||
|
|
|> filter(fn: (r) => r["_measurement"] == "{influx_measurement}")
|
|||
|
|
|> filter(fn: (r) => r["_field"] == "{field_name}")
|
|||
|
|
|> filter(fn: (r) => true){tag_filters}
|
|||
|
|
|> sort(columns: ["_time"])
|
|||
|
|
|> last()
|
|||
|
|
|> yield(name: "instantaneous")
|
|||
|
|
'''.strip()
|
|||
|
|
|
|||
|
|
with warnings.catch_warnings():
|
|||
|
|
warnings.simplefilter("ignore", MissingPivotFunction)
|
|||
|
|
frames = query_api.query_data_frame(flux)
|
|||
|
|
|
|||
|
|
if isinstance(frames, list):
|
|||
|
|
df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
|||
|
|
else:
|
|||
|
|
df = frames
|
|||
|
|
|
|||
|
|
# 获取瞬时值(最近的一个数据点)
|
|||
|
|
if df.empty or '_value' not in df.columns:
|
|||
|
|
LOGGER.debug("No instantaneous value found for field=%s", field_name)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 取第一行的值(因为查询已经排序并取了last())
|
|||
|
|
instant_value = df['_value'].iloc[0]
|
|||
|
|
if pd.isna(instant_value):
|
|||
|
|
LOGGER.debug("Instantaneous value is NaN for field=%s", field_name)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
value = float(instant_value)
|
|||
|
|
|
|||
|
|
# 如果有时间信息,记录实际的数据时间点
|
|||
|
|
if '_time' in df.columns:
|
|||
|
|
actual_time = df['_time'].iloc[0]
|
|||
|
|
LOGGER.debug("Field=%s instantaneous_value=%.3f actual_time=%s", field_name, value, actual_time)
|
|||
|
|
else:
|
|||
|
|
LOGGER.debug("Field=%s instantaneous_value=%.3f", field_name, value)
|
|||
|
|
|
|||
|
|
return value
|
|||
|
|
except Exception as e:
|
|||
|
|
LOGGER.error("Error querying InfluxDB for field=%s: %s", field_name, e)
|
|||
|
|
return None
|
|||
|
|
finally:
|
|||
|
|
try:
|
|||
|
|
client.close()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_default_temperature(field_name: str, filters: Optional[Dict[str, Any]] = None) -> float:
|
|||
|
|
"""为每个测试部位返回默认温度值(用于测试或演示)"""
|
|||
|
|
data_type = ""
|
|||
|
|
if filters:
|
|||
|
|
data_type = str(filters.get("data_type", ""))
|
|||
|
|
|
|||
|
|
defaults: Dict[tuple, float] = {
|
|||
|
|
("主轴承#1", ""): 25.5,
|
|||
|
|
("主轴承#2", ""): 26.0,
|
|||
|
|
("主轴承#3", ""): 25.8,
|
|||
|
|
("主轴承#4", ""): 26.2,
|
|||
|
|
("主轴承#5", ""): 25.9,
|
|||
|
|
("主轴承#6", ""): 26.1,
|
|||
|
|
("十字头#1", ""): 28.0,
|
|||
|
|
("十字头#2", ""): 28.2,
|
|||
|
|
("十字头#3", ""): 27.8,
|
|||
|
|
("十字头#4", ""): 28.1,
|
|||
|
|
("十字头#5", ""): 27.9,
|
|||
|
|
("减速箱小轴承#1", ""): 30.0,
|
|||
|
|
("减速箱小轴承#2", ""): 30.2,
|
|||
|
|
("减速箱大轴承#3", ""): 29.5,
|
|||
|
|
("减速箱大轴承#4", ""): 29.8,
|
|||
|
|
("mean", "润滑油温"): 35.0,
|
|||
|
|
("mean", "润滑油压"): 50.0,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return defaults.get((field_name, data_type), 25.0) # 默认25.0度
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _build_cells(
|
|||
|
|
time_slots: List[str],
|
|||
|
|
sections: List[Dict[str, Any]],
|
|||
|
|
motor_speed: str,
|
|||
|
|
start_time: Optional[datetime] = None,
|
|||
|
|
end_time: Optional[datetime] = None,
|
|||
|
|
temperature_data: Optional[Dict[str, Dict[str, float]]] = None,
|
|||
|
|
use_defaults: bool = False,
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
cells: List[Dict[str, Any]] = []
|
|||
|
|
|
|||
|
|
def add_cell(row: int, col: int, value: str = "", rowspan: int = 1, colspan: int = 1) -> None:
|
|||
|
|
payload: Dict[str, Any] = {"row": row, "col": col, "value": value}
|
|||
|
|
if rowspan > 1:
|
|||
|
|
payload["rowspan"] = rowspan
|
|||
|
|
if colspan > 1:
|
|||
|
|
payload["colspan"] = colspan
|
|||
|
|
cells.append(payload)
|
|||
|
|
|
|||
|
|
# 模板左侧标题列已经去除,这里仅生成纯数据区,从 (0,0) 开始填入数值。
|
|||
|
|
# current_row 对应模板中的实际数据行索引。
|
|||
|
|
current_row = 0
|
|||
|
|
for section in sections:
|
|||
|
|
entries = section.get("entries") or []
|
|||
|
|
if not entries:
|
|||
|
|
continue
|
|||
|
|
# 每个测试部位子项对应模板中的一行
|
|||
|
|
for entry in entries:
|
|||
|
|
# 支持新格式(带 field 映射)和旧格式(纯字符串)
|
|||
|
|
if isinstance(entry, dict):
|
|||
|
|
field_name = entry.get("field", "")
|
|||
|
|
entry_filters = entry.get("filters")
|
|||
|
|
entry_key = entry.get("result_key") or field_name
|
|||
|
|
else:
|
|||
|
|
field_name = ""
|
|||
|
|
entry_filters = None
|
|||
|
|
entry_key = ""
|
|||
|
|
|
|||
|
|
# 仅输出数值列:列索引直接对应时间段
|
|||
|
|
# 强制填充所有列,优先使用查询数据,否则使用默认值
|
|||
|
|
if field_name:
|
|||
|
|
# 根据配置决定是否启用默认值
|
|||
|
|
default_base_value: Optional[float] = None
|
|||
|
|
if use_defaults:
|
|||
|
|
default_base_value = _get_default_temperature(field_name, entry_filters)
|
|||
|
|
target_key = entry_key or field_name
|
|||
|
|
|
|||
|
|
# 遍历所有时间段列,确保每一列都有数据
|
|||
|
|
for col_idx, slot in enumerate(time_slots):
|
|||
|
|
value = None
|
|||
|
|
|
|||
|
|
# 优先使用查询到的数据
|
|||
|
|
if temperature_data:
|
|||
|
|
slot_data = temperature_data.get(target_key, {})
|
|||
|
|
if slot_data:
|
|||
|
|
slot_key = f"{col_idx}_{slot}"
|
|||
|
|
value = slot_data.get(slot_key)
|
|||
|
|
|
|||
|
|
if value is None and use_defaults and default_base_value is not None:
|
|||
|
|
# 使用基础默认值 + 时间段偏移(每个时间段增加0.1度)
|
|||
|
|
time_offset = col_idx * 0.1
|
|||
|
|
value = default_base_value + time_offset
|
|||
|
|
|
|||
|
|
if value is None:
|
|||
|
|
value_str = ""
|
|||
|
|
else:
|
|||
|
|
# 格式化为字符串(保留1位小数)
|
|||
|
|
value_str = f"{value:.1f}"
|
|||
|
|
|
|||
|
|
add_cell(current_row, col_idx, value_str)
|
|||
|
|
else:
|
|||
|
|
# 如果没有字段名,填充空字符串
|
|||
|
|
for col_idx in range(len(time_slots)):
|
|||
|
|
add_cell(current_row, col_idx, "")
|
|||
|
|
current_row += 1
|
|||
|
|
|
|||
|
|
return cells
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_temperature_data(
|
|||
|
|
time_slots: List[str],
|
|||
|
|
sections: List[Dict[str, Any]],
|
|||
|
|
start_time: Optional[datetime],
|
|||
|
|
end_time: Optional[datetime],
|
|||
|
|
) -> Dict[str, Dict[str, float]]:
|
|||
|
|
"""从 InfluxDB 查询所有测试部位在各时间点的瞬时温度值"""
|
|||
|
|
if not start_time or not end_time:
|
|||
|
|
LOGGER.info("Skip data query: missing start/end (%s, %s)", start_time, end_time)
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
influx_url = os.environ.get("INFLUX_URL", "").strip()
|
|||
|
|
influx_org = os.environ.get("INFLUX_ORG", "").strip()
|
|||
|
|
influx_token = os.environ.get("INFLUX_TOKEN", "").strip()
|
|||
|
|
influx_bucket = os.environ.get("INFLUX_BUCKET", "PCM").strip()
|
|||
|
|
influx_measurement = os.environ.get("INFLUX_MEASUREMENT", "PCM_Measurement").strip()
|
|||
|
|
|
|||
|
|
if not all([influx_url, influx_org, influx_token, influx_bucket, influx_measurement]):
|
|||
|
|
LOGGER.warning(
|
|||
|
|
"Skip data query: missing Influx config url=%s bucket=%s measurement=%s",
|
|||
|
|
influx_url or "<empty>",
|
|||
|
|
influx_bucket or "<empty>",
|
|||
|
|
influx_measurement or "<empty>",
|
|||
|
|
)
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
# 计算总时长(小时)
|
|||
|
|
total_duration = (end_time - start_time).total_seconds() / 3600.0
|
|||
|
|
LOGGER.info(
|
|||
|
|
"Fetch instantaneous temperature data window=%s→%s total_hours=%.3f time_points=%s",
|
|||
|
|
start_time.isoformat(),
|
|||
|
|
end_time.isoformat(),
|
|||
|
|
total_duration,
|
|||
|
|
",".join(time_slots),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 收集所有需要查询的字段
|
|||
|
|
query_targets: List[Tuple[str, Dict[str, Any]]] = []
|
|||
|
|
for section in sections:
|
|||
|
|
entries = section.get("entries") or []
|
|||
|
|
for entry in entries:
|
|||
|
|
if isinstance(entry, dict):
|
|||
|
|
field_name = entry.get("field", "")
|
|||
|
|
if field_name:
|
|||
|
|
query_targets.append((field_name, entry))
|
|||
|
|
|
|||
|
|
if not query_targets:
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
# 为每个时间点查询瞬时数据
|
|||
|
|
# 时间刻度采用累计写法(0.5h、1h、1.5h…),每个时间点查询对应时刻的瞬时值
|
|||
|
|
temperature_data: Dict[str, Dict[str, float]] = {}
|
|||
|
|
|
|||
|
|
# 计算每个时间点的偏移(小时)
|
|||
|
|
# 0.5h表示实验开始后0.5小时的时间点,1h表示实验开始后1小时的时间点,以此类推
|
|||
|
|
slot_durations = [_parse_time_slot(slot) for slot in time_slots]
|
|||
|
|
positive_slot_durations = [hours for hours in slot_durations if hours > 0]
|
|||
|
|
max_slot_hours = max(positive_slot_durations) if positive_slot_durations else 0.0
|
|||
|
|
force_start_based_window = bool(positive_slot_durations) and (total_duration + 1e-9 < max_slot_hours)
|
|||
|
|
if force_start_based_window:
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Total duration %.3fh shorter than max slot %.3fh, fall back to start-based windows",
|
|||
|
|
total_duration,
|
|||
|
|
max_slot_hours,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
prev_slot_end = 0.0
|
|||
|
|
for idx, slot_str in enumerate(time_slots):
|
|||
|
|
slot_hours = slot_durations[idx] if idx < len(slot_durations) else _parse_time_slot(slot_str)
|
|||
|
|
if slot_hours <= 0:
|
|||
|
|
LOGGER.debug("Skip slot index=%d label=%s because parsed hours<=0", idx, slot_str)
|
|||
|
|
continue
|
|||
|
|
if force_start_based_window:
|
|||
|
|
slot_start_offset = prev_slot_end
|
|||
|
|
slot_end_offset = slot_hours
|
|||
|
|
else:
|
|||
|
|
# 限制在实验的有效范围内
|
|||
|
|
slot_end_offset = min(slot_hours, total_duration)
|
|||
|
|
slot_start_offset = min(prev_slot_end, total_duration)
|
|||
|
|
if slot_end_offset <= slot_start_offset:
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Skip slot index=%d label=%s because end_offset<=start_offset (%.3f<=%.3f)",
|
|||
|
|
idx,
|
|||
|
|
slot_str,
|
|||
|
|
slot_end_offset,
|
|||
|
|
slot_start_offset,
|
|||
|
|
)
|
|||
|
|
prev_slot_end = max(prev_slot_end, slot_end_offset)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
slot_start = start_time + timedelta(hours=slot_start_offset)
|
|||
|
|
slot_end = start_time + timedelta(hours=slot_end_offset)
|
|||
|
|
|
|||
|
|
if not force_start_based_window:
|
|||
|
|
# 确保不超过总结束时间
|
|||
|
|
if slot_start >= end_time:
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Skip slot index=%d label=%s start>=end (%s>=%s)",
|
|||
|
|
idx,
|
|||
|
|
slot_str,
|
|||
|
|
slot_start.isoformat(),
|
|||
|
|
end_time.isoformat(),
|
|||
|
|
)
|
|||
|
|
continue
|
|||
|
|
if slot_end > end_time:
|
|||
|
|
slot_end = end_time
|
|||
|
|
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Slot index=%d label=%s offsets=%.3f→%.3f actual=%s→%s",
|
|||
|
|
idx,
|
|||
|
|
slot_str,
|
|||
|
|
slot_start_offset,
|
|||
|
|
slot_end_offset,
|
|||
|
|
slot_start.isoformat(),
|
|||
|
|
slot_end.isoformat(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if force_start_based_window:
|
|||
|
|
prev_slot_end = max(prev_slot_end, slot_hours)
|
|||
|
|
else:
|
|||
|
|
prev_slot_end = max(prev_slot_end, slot_end_offset)
|
|||
|
|
|
|||
|
|
# 查询每个字段在当前时间点的瞬时值
|
|||
|
|
# 使用slot_end作为目标时间点(即累计时间点,如0.5h, 1h, 1.5h等)
|
|||
|
|
target_time_point = slot_end
|
|||
|
|
|
|||
|
|
for field_name, entry in query_targets:
|
|||
|
|
result_key = entry.get("result_key") or field_name
|
|||
|
|
if not result_key:
|
|||
|
|
result_key = field_name
|
|||
|
|
entry_filters = entry.get("filters") if isinstance(entry, dict) else None
|
|||
|
|
if result_key not in temperature_data:
|
|||
|
|
temperature_data[result_key] = {}
|
|||
|
|
|
|||
|
|
# 使用索引作为key,因为可能有重复的时间刻度
|
|||
|
|
slot_key = f"{idx}_{slot_str}" # 使用索引+时间刻度作为唯一key
|
|||
|
|
|
|||
|
|
# 查询瞬时值:传入相同的时间点作为start和end,函数内部会处理为查找最近的数据点
|
|||
|
|
value = _query_influxdb(
|
|||
|
|
field_name,
|
|||
|
|
target_time_point, # start_time参数(函数内部不使用)
|
|||
|
|
target_time_point, # end_time参数(作为目标时间点)
|
|||
|
|
influx_url,
|
|||
|
|
influx_org,
|
|||
|
|
influx_token,
|
|||
|
|
influx_bucket,
|
|||
|
|
influx_measurement,
|
|||
|
|
filters=entry_filters if entry_filters else None,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if value is not None:
|
|||
|
|
temperature_data[result_key][slot_key] = value
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Field=%s result_key=%s slot=%s value=%.3f",
|
|||
|
|
field_name,
|
|||
|
|
result_key,
|
|||
|
|
slot_key,
|
|||
|
|
value,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
LOGGER.debug(
|
|||
|
|
"Field=%s result_key=%s slot=%s yielded no data",
|
|||
|
|
field_name,
|
|||
|
|
result_key,
|
|||
|
|
slot_key,
|
|||
|
|
)
|
|||
|
|
prev_slot_end = slot_end_offset
|
|||
|
|
return temperature_data
|
|||
|
|
def build_temperature_table(_: Dict[str, Any]) -> Dict[str, Any]:
|
|||
|
|
token = os.environ.get("TABLE_TOKEN", "scriptTable1")
|
|||
|
|
row_offset = int(os.environ.get("TABLE_START_ROW", "0") or 0)
|
|||
|
|
col_offset = int(os.environ.get("TABLE_START_COL", "0") or 0)
|
|||
|
|
motor_speed = os.environ.get("TABLE_MOTOR_SPEED", "980RPM")
|
|||
|
|
# 解析实验时间范围
|
|||
|
|
start_str = os.environ.get("EXPERIMENT_START", "").strip()
|
|||
|
|
end_str = os.environ.get("EXPERIMENT_END", "").strip()
|
|||
|
|
start_time: Optional[datetime] = None
|
|||
|
|
end_time: Optional[datetime] = None
|
|||
|
|
if start_str:
|
|||
|
|
try:
|
|||
|
|
# 支持多种时间格式
|
|||
|
|
for fmt in ["%Y-%m-%dT%H:%M:%SZ"]:
|
|||
|
|
try:
|
|||
|
|
start_time = datetime.strptime(start_str, fmt).astimezone(tz=None)
|
|||
|
|
break
|
|||
|
|
except ValueError:
|
|||
|
|
continue
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Warning: Failed to parse EXPERIMENT_START '{start_str}': {e}", file=sys.stderr)
|
|||
|
|
if end_str:
|
|||
|
|
try:
|
|||
|
|
for fmt in ["%Y-%m-%dT%H:%M:%SZ"]:
|
|||
|
|
try:
|
|||
|
|
end_time = datetime.strptime(end_str, fmt).astimezone(tz=None)
|
|||
|
|
break
|
|||
|
|
except ValueError:
|
|||
|
|
continue
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Warning: Failed to parse EXPERIMENT_END '{end_str}': {e}", file=sys.stderr)
|
|||
|
|
time_slots = _time_slots()
|
|||
|
|
sections = _default_sections()
|
|||
|
|
# 查询温度数据
|
|||
|
|
temperature_data = _load_temperature_data(time_slots, sections, start_time, end_time)
|
|||
|
|
# 始终禁止默认数据,保证查询不到值时保持空白
|
|||
|
|
use_defaults = False
|
|||
|
|
cells = _build_cells(
|
|||
|
|
time_slots,
|
|||
|
|
sections,
|
|||
|
|
motor_speed,
|
|||
|
|
start_time,
|
|||
|
|
end_time,
|
|||
|
|
temperature_data,
|
|||
|
|
use_defaults=use_defaults
|
|||
|
|
)
|
|||
|
|
for cell in cells:
|
|||
|
|
cell["row"] += 4
|
|||
|
|
start_time_row = 1
|
|||
|
|
start_time_value_col = 1
|
|||
|
|
end_time_value_col = 3
|
|||
|
|
utc_aware_dt = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S%z")
|
|||
|
|
local_dt1 = utc_aware_dt.astimezone(tz=None)
|
|||
|
|
local_dt2 = utc_aware_dt.astimezone(tz=None) + timedelta(hours=3.5)
|
|||
|
|
start_time_value = local_dt1.strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
end_time_value = local_dt2.strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
cells.append({"row": start_time_row, "col": start_time_value_col, "value": start_time_value})
|
|||
|
|
cells.append({"row": start_time_row, "col": end_time_value_col, "value": end_time_value})
|
|||
|
|
influx_url = os.environ.get("INFLUX_URL", "").strip()
|
|||
|
|
influx_org = os.environ.get("INFLUX_ORG", "").strip()
|
|||
|
|
influx_token = os.environ.get("INFLUX_TOKEN", "").strip()
|
|||
|
|
influx_bucket = os.environ.get("INFLUX_BUCKET", "PCM").strip()
|
|||
|
|
influx_measurement = os.environ.get("INFLUX_MEASUREMENT", "PCM_Measurement").strip()
|
|||
|
|
value = _query_influxdb(
|
|||
|
|
"环境温度",
|
|||
|
|
start_time,
|
|||
|
|
end_time,
|
|||
|
|
influx_url,
|
|||
|
|
influx_org,
|
|||
|
|
influx_token,
|
|||
|
|
influx_bucket,
|
|||
|
|
influx_measurement,
|
|||
|
|
filters={"data_type": "LSDAQ"},
|
|||
|
|
)
|
|||
|
|
# 确保value不是None,避免Word COM操作异常
|
|||
|
|
if value is not None:
|
|||
|
|
cells.append({"row": 0, "col": 1, "value": f"{value:.1f}"})
|
|||
|
|
else:
|
|||
|
|
cells.append({"row": 0, "col": 1, "value": ""})
|
|||
|
|
return {
|
|||
|
|
"token": token,
|
|||
|
|
"startRow": row_offset,
|
|||
|
|
"startCol": col_offset,
|
|||
|
|
"cells": cells,
|
|||
|
|
}
|
|||
|
|
def main() -> int:
|
|||
|
|
try:
|
|||
|
|
try:
|
|||
|
|
if not logging.getLogger().handlers:
|
|||
|
|
log_level_name = os.environ.get("TABLE_LOG_LEVEL", "DEBUG").strip() or "DEBUG"
|
|||
|
|
log_level = getattr(logging, log_level_name.upper(), logging.DEBUG)
|
|||
|
|
log_file_raw = os.environ.get("TABLE_LOG_FILE", "test.log").strip() or "test.log"
|
|||
|
|
log_file = os.path.abspath(log_file_raw)
|
|||
|
|
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=log_level,
|
|||
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|||
|
|
handlers=[
|
|||
|
|
logging.FileHandler(log_file, encoding="utf-8"),
|
|||
|
|
logging.StreamHandler(sys.stderr),
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
LOGGER.info("Logging initialized -> file=%s level=%s", log_file, logging.getLevelName(log_level))
|
|||
|
|
_log_environment_variables()
|
|||
|
|
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
payload = _load_payload()
|
|||
|
|
table_spec = build_temperature_table(payload)
|
|||
|
|
result = {"tables": [table_spec]}
|
|||
|
|
print(json.dumps(result, ensure_ascii=False))
|
|||
|
|
return 0
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"error: {exc}", file=sys.stderr)
|
|||
|
|
return 1
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main())
|