import hashlib import json import multiprocessing import os import sys import time from datetime import datetime from typing import Dict, List, Tuple from urllib.parse import urlparse, parse_qs import pandas as pd import requests class EnosAPIClient: TOKEN_CACHE_FILE = "enos_token_cache.json" def __init__(self, app_key: str, app_secret: str, apigw_address: str, org_id: str, is_edge: bool = True, cache_dir: str = None): self.app_key = app_key self.app_secret = app_secret self.apigw_address = apigw_address self.org_id = org_id self.protocol = "http" if is_edge else "https" self.cache_dir = cache_dir or os.path.join(os.getcwd(), ".enos_cache") os.makedirs(self.cache_dir, exist_ok=True) self.cache_file = os.path.join(self.cache_dir, self.TOKEN_CACHE_FILE) self.access_token = None self.token_expire_time = 0 self._load_token_from_cache() def _sha256_lower(self, text: str) -> str: return hashlib.sha256(text.encode('utf-8')).hexdigest().lower() def _load_token_from_cache(self) -> bool: try: if not os.path.exists(self.cache_file): return False with open(self.cache_file, 'r', encoding='utf-8') as f: cache_data = json.load(f) cache_key = f"{self.app_key}_{self.apigw_address}" token_info = cache_data.get(cache_key) if token_info and time.time() < token_info.get('expire_time', 0) - 300: self.access_token = token_info.get('access_token') self.token_expire_time = token_info.get('expire_time') return True return False except: return False def _save_token_to_cache(self, access_token: str, expire_seconds: int): try: cache_data = {} if os.path.exists(self.cache_file): with open(self.cache_file, 'r', encoding='utf-8') as f: cache_data = json.load(f) cache_key = f"{self.app_key}_{self.apigw_address}" cache_data[cache_key] = { 'access_token': access_token, 'expire_time': time.time() + expire_seconds } with open(self.cache_file, 'w', encoding='utf-8') as f: json.dump(cache_data, f, indent=2) except: pass def get_access_token(self, force_refresh: bool = False) -> Tuple[bool, str]: if not force_refresh and self.access_token and time.time() < self.token_expire_time - 300: return True, "使用缓存Token" timestamp = int(time.time() * 1000) encryption = self._sha256_lower(f"{self.app_key}{timestamp}{self.app_secret}") url = f"{self.protocol}://{self.apigw_address}/apim-token-service/v2.0/token/get" try: resp = requests.post(url, json={ "appKey": self.app_key, "encryption": encryption, "timestamp": timestamp }, timeout=10) result = resp.json() if result.get("status") == 0: token_data = result.get("data", {}) self.access_token = token_data.get("accessToken") expire = token_data.get("expire", 7200) self.token_expire_time = time.time() + expire self._save_token_to_cache(self.access_token, expire) return True, "获取Token成功" return False, result.get("msg", "未知错误") except Exception as e: return False, str(e) def _generate_signature(self, url: str, body: str = "") -> Dict: parsed = urlparse(url) params = parse_qs(parsed.query) sorted_keys = sorted(params.keys()) params_data = "".join([f"{k}{params[k][0]}" for k in sorted_keys if params[k]]) if body: params_data += body timestamp = str(int(time.time() * 1000)) sign_data = f"{self.access_token}{params_data}{timestamp}{self.app_secret}" apim_sign = self._sha256_lower(sign_data) return { "apim-accesstoken": self.access_token, "apim-signature": apim_sign, "apim-timestamp": timestamp, "Content-Type": "application/json; charset=utf-8" } def call_api(self, url: str, method: str = "GET", body: Dict = None) -> Tuple[bool, Dict]: if not self.access_token or time.time() >= self.token_expire_time: success, msg = self.get_access_token(force_refresh=True) if not success: return False, {"error": f"Token获取失败: {msg}"} body_str = json.dumps(body, ensure_ascii=False) if body else "" headers = self._generate_signature(url, body_str) try: if method.upper() == "GET": resp = requests.get(url, headers=headers, timeout=30) else: resp = requests.post(url, headers=headers, data=body_str.encode('utf-8'), timeout=30) result = resp.json() if result.get("code") == 0: return True, result return False, result except Exception as e: return False, {"error": str(e)} def split_list(lst: List, chunk_size: int) -> List[List]: """将列表按指定大小分组""" return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)] def query_historical_data(client: EnosAPIClient, mdm_id: str, point_ids: List[str], start_time: str, end_time: str, interval: str = "RAW", retry_times=0) -> Tuple[ bool, List[Dict]]: """ 查询历史测点数据(支持测点自动分批) """ all_items = [] point_groups = split_list(point_ids, 20) # 每20个测点一组 print(f"共 {len(point_ids)} 个测点,分 {len(point_groups)} 批查询") for idx, group in enumerate(point_groups, 1): points_str = ",".join(group) url = (f"{client.protocol}://{client.apigw_address}/cds-timeseries-service/v1.0/tsdb-detail" f"?action=query" f"&orgId={client.org_id}" f"&mdmIds={mdm_id}" f"&pointIdsWithLogic={points_str}" f"&startTime={start_time}" f"&endTime={end_time}" f"&interval={interval}") success, result = client.call_api(url, "GET") if success: items = result.get("data", {}).get("items", []) all_items.extend(items) print(f"批次 {idx}/{len(point_groups)}:获取 {len(items)} 条记录") else: if retry_times <= 5: retry_times = retry_times + 1 error_msg = result.get("error", result.get("msg", "未知错误")) print(f"批次 {idx}/{len(point_groups)}: 查询失败 - {error_msg},次数: {retry_times}") query_historical_data(client, mdm_id, point_ids, start_time, end_time, interval, retry_times) time.sleep(0.01) else: print(f"风机:{mdm_id}, {len(point_groups)}, 查询{retry_times}仍失败") return False, [] return True, all_items def get_prev_day(date: str | None | datetime = None, prev_days: int = 1) -> str: """ 获取上一天日期 :param date: 日期字符串,格式为YYYY-MM-DD :param prev_days: 上几天,默认1天 :return: 上几天日期字符串,格式为YYYY-MM-DD ,例如:2026-06-01 """ if not date: date = datetime.now().strftime("%Y-%m-%d") date = pd.to_datetime(date) prev_day = date - pd.Timedelta(days=prev_days) return prev_day.strftime("%Y-%m-%d") def get_prev_days(date: str | None | datetime = None, prev_days: int = 1) -> list: """ 获取上一天日期 :param date: 日期字符串,格式为YYYY-MM-DD :param prev_days: 上几天,默认1天 :return: 上几天日期字符串,格式为YYYY-MM-DD ,例如:2026-06-01 """ if not date: date = datetime.now().strftime("%Y-%m-%d") date = pd.to_datetime(date) prev_datas = [] if prev_days > 0: for i in range(prev_days, 0, -1): prev_datas.append((date - pd.Timedelta(days=i)).strftime("%Y-%m-%d")) else: for i in range(0, -prev_days + 1): prev_datas.append((date + pd.Timedelta(days=i)).strftime("%Y-%m-%d")) return prev_datas def query_by_day(client: EnosAPIClient, mdm_id: str, point_ids: List[str], query_date: str) -> None: """ 按天分批查询数据 """ json_datas = {} day_start = f"{query_date} 00:00:00" day_end = f"{query_date} 23:59:59" print(f"查询日期: {query_date}") success, items = query_historical_data( client, mdm_id, point_ids, day_start, day_end, "RAW" ) if success: for item in items: localtime = item.get("localtime") if not localtime in json_datas.keys(): json_datas[localtime] = {} json_datas[localtime].update(item) print(f"当天总计: {len(items)} 条记录") else: print(f"查询失败: {items}") # 保存所有数据 if json_datas: df = pd.DataFrame().from_dict(json_datas.values()) filename = f"{mdm_id}_{query_date}.parquet" df.to_parquet(filename, index=False) print(f"总共获取 {df.shape} 条记录,已保存到 {filename}") else: print(" 未获取到任何数据") def get_wind_farm(read_db): df_wind_farm = pd.read_csv('wind_farm.csv', encoding='utf-8') df_wind_farm_db = df_wind_farm[df_wind_farm['store_id'] == read_db] return df_wind_farm_db def get_wind_turbine(): pass # ===================== 使用示例 ===================== def main(read_db): # 配置参数(请替换为实际值) APP_KEY = "a6af2233-227e-4684-ab59-9e5f74e5718c" APP_SECRET = "56f24801-0429-486f-b4db-ba61a33a101c" API_GW_ADDRESS = "ag-cdt1.eniot.io" ORG_ID = "o16021383932361" # 设备ID列表 MDM_IDS = ["03puSZ72"] # 测点ID列表(100多个测点,会自动按20个一组分批) POINT_IDS = "last(WCNV.DECCNVAI005),last(WCNV.DECCNVAI008),last(WCNV.DECCNVAI006),last(WCNV.DECCNVAI009),last(WCNV.DECCNVAI007),last(WCNV.DECCNVAI010),last(WTRM.TrmTmpShfBrg),first(WYAW.DECYAWDI004),last(WYAW.DECYAWAI001),first(WTUR.TurbineSts),first(WTUR.TurbineSts_Map),last(WGEN.GenSpd),last(WGEN.TemGenNonDE),last(WGEN.TemGenDriEnd),last(WROT.TemAxis1Ctrl),last(WROT.TemAxis2Ctrl),last(WROT.TemAxis3Ctrl),last(WROT.TemB1Mot),last(WROT.TemB2Mot),last(WROT.TemB3Mot),last(WROT.CurBlade1Motor),last(WROT.CurBlade2Motor),last(WROT.CurBlade3Motor),last(WROT.DECROTAI004),last(WROT.DECROTAI005),last(WROT.DECROTAI006),last(WROT.Blade1Speed),last(WROT.Blade2Speed),last(WROT.Blade3Speed),last(WCNV.DECCNVAI012),last(WCNV.DECCNVAI011),last(WTOW.TemTower),last(WTUR.DECTURAI016),last(WTUR.DECTURAI015),first(WTUR.DECTURDI030),last(WGEN.TemGenStaU),last(WGEN.TemGenStaV),last(WGEN.TemGenStaW),last(WGEN.Torque),last(WYAW.TotalTwist),first(WTUR.AIStatusCode),last(WGEN.GenReactivePW),last(WGEN.GenActivePW),last(WTUR.DECTURAI003),last(WYAW.NacellePosition),last(WNAC.TemNacelle),last(WNAC.TemOut),last(WNAC.TemNacelleCab),last(WROT.Blade1Position),last(WROT.Blade2Position),last(WROT.Blade3Position),last(WROT.Blade1Setpoint),last(WROT.Blade2Setpoint),last(WROT.Blade3Setpoint),last(WTRM.PowerStoreTRBS),last(WNAC.DECNACAI004),last(WNAC.TheoryActivePW),last(WCNV.VolConL1),last(WCNV.CurConL1),last(WCNV.VolConL2),last(WCNV.CurConL2),last(WCNV.VolConL3),last(WCNV.CurConL3),last(WGEN.TorqueSetpoint),last(WNAC.DECNACAI003),last(WCNV.GridFreq),first(WYAW.YawCCWSts),first(WTUR.CCTurbineSts),first(WYAW.YawCWSts),last(WNAC.WindDirection),first(WTUR.GenState),last(WTRM.RotorSpd),last(WNAC.WindSpeed),last(WTRM.TemGeaMSND),last(WTRM.TemGeaMSDE),last(WTRM.GBoxOilPmpP),last(WTRM.TemGeaOil),last(WTRM.GBoxSpd)".split( ",") POINT_IDS = POINT_IDS[0:15] # 初始化客户端 client = EnosAPIClient(APP_KEY, APP_SECRET, API_GW_ADDRESS, ORG_ID) # 获取Token success, msg = client.get_access_token() if not success: print(f"error: {msg}") return print(f"success: {msg}") for query_date in get_prev_days(prev_days=1): with multiprocessing.Pool(6, maxtasksperchild=5) as pool: # 查询数据 pool.starmap(query_by_day, [(client, mdm_id, POINT_IDS, query_date) for mdm_id in MDM_IDS]) if __name__ == "__main__": read_db = 1 args = sys.argv if len(args) > 1: read_db = int(args[1]) print(read_db) main(read_db)