# ========================================== # crawlers/nrems.py - NREMS 크롤러 (1,2,3,4,9호기) # ========================================== import requests import json import re from datetime import datetime from .base import safe_float, create_session, format_result def _get_inverter_sums(session, pscode, system_config): """ 1, 2호기 인버터별 일일 발전량 추출 (JSON API 사용) """ try: today_str = datetime.now().strftime('%Y-%m-%d') month_str = datetime.now().strftime('%Y-%m') headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Referer': f'http://www.nrems.co.kr/v2/local/comp/cp_inv_time.php?pscode={pscode}' } data = { 'act': 'getList', 's_day': today_str, 's_date': today_str, 'e_date': today_str, 's_mon': month_str, 'e_mon': month_str, 'pscode': pscode, 'dispType': 'time' } inv_proc_url = system_config.get('inv_proc_url', '') res = session.post(inv_proc_url, data=data, headers=headers, timeout=10) if res.status_code == 200: try: json_data = res.json() invlist = json_data.get('invlist', []) sum_1 = 0.0 sum_2 = 0.0 for inv in invlist: tidx = str(inv.get('tidx', '')) sum_pw = safe_float(inv.get('sumPw')) if tidx == '1': sum_1 = sum_pw elif tidx == '2': sum_2 = sum_pw if sum_1 > 0 or sum_2 > 0: print(f" [API] 인버터 합계 추출 성공! (인버터1: {sum_1} kWh / 인버터2: {sum_2} kWh)") return sum_1, sum_2 else: print(f" ⚠️ API 응답에 인버터 데이터 없음") return 0.0, 0.0 except json.JSONDecodeError: print(f" ⚠️ JSON 파싱 실패") return 0.0, 0.0 else: print(f" ⚠️ API 응답 오류: {res.status_code}") return 0.0, 0.0 except Exception as e: print(f" [에러] {e}") return 0.0, 0.0 def fetch_data(plant_info): """ NREMS 발전소 데이터 수집 Args: plant_info: { 'id': 'nrems-03', # DB용 고유 ID (is_split인 경우 없음) 'name': '...', 'type': 'nrems', 'auth': {'pscode': '...'}, 'options': {'is_split': True/False}, 'system': {'api_url': '...', 'inv_proc_url': '...'}, 'company_name': '...' } Returns: list: [{'id': '...', 'name': '...', 'kw': 10.5, 'today': 100.0, 'status': '...'}] """ results = [] # 설정 추출 plant_id = plant_info.get('id', '') # DB용 고유 ID pscode = plant_info['auth'].get('pscode', '') is_split = plant_info['options'].get('is_split', False) system_config = plant_info.get('system', {}) company_name = plant_info.get('company_name', '태양과바람') plant_name = plant_info.get('name', '') session = create_session() headers = { 'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } try: # 메인 데이터 요청 api_url = system_config.get('api_url', '') res = session.post(api_url, data={'pscode': pscode}, headers=headers, timeout=10) if res.status_code != 200: return results try: data = res.json() except: return results # 데이터 찾기 ps_list = data.get('ps_status') target_data = None if isinstance(ps_list, list): for item in ps_list: code_in_res = item.get('pscode') wmu_in_res = item.get('WMU_CODE') # Case-insensitive comparison if (code_in_res and code_in_res.lower() == pscode.lower()) or \ (wmu_in_res and wmu_in_res.lower() == pscode.lower()): target_data = item break if not target_data and len(ps_list) > 0: print(f" ⚠️ Target pscode '{pscode}' not found in response. Available: {[i.get('pscode') for i in ps_list]}") target_data = ps_list[0] # Fallback print(f" ⚠️ Using fallback: {target_data.get('pscode')}") elif isinstance(ps_list, dict): target_data = ps_list if not target_data: target_data = {} total_kw = safe_float(target_data.get('KW')) total_today = safe_float(target_data.get('TDayKWH')) inverters = data.get('ivt_value', []) # Case A: 1, 2호기 분리 처리 if is_split: real_sum_1, real_sum_2 = _get_inverter_sums(session, pscode, system_config) kw_1 = safe_float(inverters[0].get('KW')) if len(inverters) >= 1 else 0.0 kw_2 = safe_float(inverters[1].get('KW')) if len(inverters) >= 2 else 0.0 if (real_sum_1 + real_sum_2) > 0: today_1 = real_sum_1 today_2 = real_sum_2 else: print(" ⚠️ 백업 로직(비율) 가동") inv_total = kw_1 + kw_2 if inv_total > 0: today_1 = total_today * (kw_1 / inv_total) today_2 = total_today * (kw_2 / inv_total) else: today_1 = total_today / 2 today_2 = total_today / 2 # [중요] 1, 2호기는 ID를 강제 지정 results.append({ 'id': 'nrems-01', # 1호기 고정 ID 'name': f'{company_name} 1호기', 'kw': kw_1, 'today': round(today_1, 2), 'status': "🟢 정상" if kw_1 > 0 else "💤 대기" }) results.append({ 'id': 'nrems-02', # 2호기 고정 ID 'name': f'{company_name} 2호기', 'kw': kw_2, 'today': round(today_2, 2), 'status': "🟢 정상" if kw_2 > 0 else "💤 대기" }) # Case B: 3, 4, 9호기 else: results.append({ 'id': plant_id, # config에서 정의된 ID 사용 'name': f'{company_name} {plant_name}', 'kw': total_kw, 'today': total_today, 'status': "🟢 정상" if total_kw > 0 else "💤 대기" }) except Exception as e: print(f"❌ NREMS {plant_name} 오류: {e}") if not is_split: results.append({ 'id': plant_id, 'name': f'{company_name} {plant_name}', 'kw': 0.0, 'today': 0.0, 'status': '🔴 오류' }) return results def fetch_history_hourly(plant_info, start_date, end_date): """ NREMS 발전소의 시간대별 과거 데이터 수집 Args: plant_info: { 'id': 'nrems-03', 'name': '...', 'type': 'nrems', 'auth': {'pscode': '...'}, 'options': {'is_split': True/False}, 'system': {'api_url': '...', 'inv_proc_url': '...'}, 'company_name': '...' } start_date: str, 시작일 (YYYY-MM-DD) end_date: str, 종료일 (YYYY-MM-DD) Returns: list: [{ 'plant_id': 'nrems-03', 'timestamp': '2026-01-15 14:00:00', 'generation_kwh': 123.5, 'current_kw': 15.2 }, ...] """ results = [] # 설정 추출 plant_id = plant_info.get('id', '') pscode = plant_info['auth'].get('pscode', '') is_split = plant_info['options'].get('is_split', False) plant_name = plant_info.get('name', '') # 날짜 범위 생성 from datetime import datetime, timedelta current_date = datetime.strptime(start_date, '%Y-%m-%d') end_dt = datetime.strptime(end_date, '%Y-%m-%d') session = create_session() print(f"\n{'='*60}") print(f"[NREMS Hourly] {plant_name} ({start_date} ~ {end_date})") print(f"{'='*60}") while current_date <= end_dt: date_str = current_date.strftime('%Y-%m-%d') print(f"\n[Processing Date] {date_str}") headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' } try: if is_split: # 1,2호기: cp_inv_proc.php with dispType=time url = 'http://www.nrems.co.kr/v2/local/proc/cp_inv_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/comp/cp_inv_time.php?pscode={pscode}' payload = { 'act': 'getList', 's_day': date_str, 's_date': date_str, 'e_date': date_str, 's_mon': date_str[:7], 'e_mon': date_str[:7], 'pscode': pscode, 'dispType': 'time' } else: # 3,4,9호기: pl_time_proc.php with act=empty url = 'http://www.nrems.co.kr/v2/local/proc/pl_time_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/plant/pl_time.php?pscode={pscode}' payload = { 'act': 'empty', 's_date': date_str, 'pscode': pscode } response = session.post(url, data=payload, headers=headers, timeout=10) if response.status_code == 200: data = response.json() # 데이터 구조 확인 if is_split: # 1,2호기: pwdata 키 사용 hourly_records = data.get('pwdata', []) else: # 3,4,9호기: pdata 키 사용 hourly_records = data.get('pdata', []) if hourly_records: print(f" ✓ Found {len(hourly_records)} hourly records") for hour_data in hourly_records: if is_split: # 1,2호기: DATE, PW1, PW2 hour = hour_data.get('DATE', '00') inv1_gen = safe_float(hour_data.get('PW1', 0)) inv2_gen = safe_float(hour_data.get('PW2', 0)) # timestamp 생성 timestamp = f"{date_str} {str(hour).zfill(2)}:00:00" results.append({ 'plant_id': 'nrems-01', 'timestamp': timestamp, 'generation_kwh': inv1_gen, 'current_kw': 0 }) results.append({ 'plant_id': 'nrems-02', 'timestamp': timestamp, 'generation_kwh': inv2_gen, 'current_kw': 0 }) else: # 3,4,9호기: TIME, INV time_str = hour_data.get('TIME', '00:00') hour = time_str.split(':')[0] # "14:00" -> "14" generation_kwh = safe_float(hour_data.get('INV', 0)) # timestamp 생성 timestamp = f"{date_str} {str(hour).zfill(2)}:00:00" results.append({ 'plant_id': plant_id, 'timestamp': timestamp, 'generation_kwh': generation_kwh, 'current_kw': 0 }) print(f" → Collected {len(hourly_records)} records") else: print(f" ⚠ No hourly data for {date_str}") else: print(f" ✗ HTTP {response.status_code}") except Exception as e: print(f" ✗ Error: {e}") current_date += timedelta(days=1) print(f"\n{'='*60}") print(f"[Total] Collected {len(results)} hourly records") print(f"{'='*60}\n") return results def fetch_history_daily(plant_info, start_date, end_date): """ NREMS 발전소의 일별 과거 데이터 수집 (월 단위 루프) Args: plant_info: 발전소 정보 start_date: str, 시작일 (YYYY-MM-DD) end_date: str, 종료일 (YYYY-MM-DD) Returns: list: [{'plant_id': '...', 'date': '2026-01-15', 'generation_kwh': 123.5}, ...] """ from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import calendar results = [] # 설정 추출 plant_id = plant_info.get('id', '') pscode = plant_info['auth'].get('pscode', '') is_split = plant_info['options'].get('is_split', False) plant_name = plant_info.get('name', '') session = create_session() print(f"\n{'='*60}") print(f"[NREMS Daily] {plant_name} ({start_date} ~ {end_date}) - Looping by Month") print(f"{'='*60}") start_dt = datetime.strptime(start_date, '%Y-%m-%d') end_dt = datetime.strptime(end_date, '%Y-%m-%d') current_dt = start_dt while current_dt <= end_dt: # 현재 처리할 달의 시작일과 종료일 계산 # 이번 달의 마지막 날 last_day_of_month = calendar.monthrange(current_dt.year, current_dt.month)[1] chunk_end_dt = current_dt.replace(day=last_day_of_month) # 요청 종료일이 전체 종료일보다 뒤면 전체 종료일로 제한 if chunk_end_dt > end_dt: chunk_end_dt = end_dt s_date_str = current_dt.strftime('%Y-%m-%d') e_date_str = chunk_end_dt.strftime('%Y-%m-%d') month_str = current_dt.strftime('%Y-%m') print(f" [Fetching] {s_date_str} ~ {e_date_str} ...", end="", flush=True) headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' } try: if is_split: # 1,2호기: cp_inv_proc.php with dispType=day url = 'http://www.nrems.co.kr/v2/local/proc/cp_inv_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/comp/cp_inv_day.php?pscode={pscode}' payload = { 'act': 'getList', 's_day': s_date_str, # s_day를 시작일로 변경 's_date': s_date_str, 'e_date': e_date_str, 's_mon': s_date_str[:7], 'e_mon': e_date_str[:7], 'pscode': pscode, 'dispType': 'day' } else: # 3,4,9호기: pl_day_proc.php with s_day/e_day range url = 'http://www.nrems.co.kr/v2/local/proc/pl_day_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/plant/pl_day.php?pscode={pscode}' payload = { 'act': 'empty', 's_day': s_date_str, 'e_day': e_date_str, 'pscode': pscode } response = session.post(url, data=payload, headers=headers, timeout=15) if response.status_code == 200: try: data = response.json() # 데이터 구조 확인 if is_split: daily_records = data.get('pwdata', []) else: daily_records = data.get('pdata', []) if daily_records: count = 0 for day_data in daily_records: # 날짜 추출 date_raw = day_data.get('DATE', '') if not date_raw: continue # 날짜 형식 변환: "12-28" -> "2025-12-28" 보정 clean_date = date_raw if '-' in date_raw and len(date_raw.split('-')[0]) <= 2: mm, dd = date_raw.split('-') year = current_dt.year # 만약 12월 데이터인데 1월에 긁으면... 루프 변수 current_dt.year 사용하면 안전 clean_date = f"{year}-{mm.zfill(2)}-{dd.zfill(2)}" if is_split: inv1_gen = safe_float(day_data.get('PW1', 0)) inv2_gen = safe_float(day_data.get('PW2', 0)) results.append({'plant_id': 'nrems-01', 'date': clean_date, 'generation_kwh': inv1_gen}) results.append({'plant_id': 'nrems-02', 'date': clean_date, 'generation_kwh': inv2_gen}) count += 1 else: generation_kwh = safe_float(day_data.get('INV', 0)) results.append({'plant_id': plant_id, 'date': clean_date, 'generation_kwh': generation_kwh}) count += 1 print(f" OK ({count} days)") else: print(f" No data") except Exception as json_err: print(f" JSON Error: {json_err}") else: print(f" HTTP {response.status_code}") except Exception as e: print(f" Error: {e}") # 다음 달 1일로 이동 current_dt = (current_dt.replace(day=1) + timedelta(days=32)).replace(day=1) print(f"\n[Total] Collected {len(results)} daily records\n") return results def fetch_history_monthly(plant_info, start_month, end_month): """ NREMS 발전소의 월별 과거 데이터 수집 Args: plant_info: 발전소 정보 start_month: str, 시작월 (YYYY-MM) end_month: str, 종료월 (YYYY-MM) Returns: list: [{'plant_id': '...', 'month': '2026-01', 'generation_kwh': 12345.6}, ...] """ from datetime import datetime results = [] # 설정 추출 plant_id = plant_info.get('id', '') pscode = plant_info['auth'].get('pscode', '') is_split = plant_info['options'].get('is_split', False) plant_name = plant_info.get('name', '') session = create_session() print(f"\n{'='*60}") print(f"[NREMS Monthly] {plant_name} ({start_month} ~ {end_month})") print(f"{'='*60}") headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' } try: if is_split: # 1,2호기: cp_inv_proc.php with dispType=mon url = 'http://www.nrems.co.kr/v2/local/proc/cp_inv_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/comp/cp_inv_month.php?pscode={pscode}' payload = { 'act': 'getList', 's_day': f"{end_month}-01", 's_date': f"{start_month}-01", 'e_date': f"{end_month}-01", 's_mon': start_month, 'e_mon': end_month, 'pscode': pscode, 'dispType': 'mon' } else: # 3,4,9호기: pl_month_proc.php with s_date/e_date (YYYY-MM) url = 'http://www.nrems.co.kr/v2/local/proc/pl_month_proc.php' headers['Referer'] = f'http://www.nrems.co.kr/v2/local/plant/pl_month.php?pscode={pscode}' payload = { 'act': 'empty', 's_date': start_month, 'e_date': end_month, 'pscode': pscode } response = session.post(url, data=payload, headers=headers, timeout=15) if response.status_code == 200: data = response.json() # 데이터 구조 확인 if is_split: # 1,2호기: pwdata 키 사용 monthly_records = data.get('pwdata', []) else: # 3,4,9호기: pdata 키 사용 monthly_records = data.get('pdata', []) if monthly_records: print(f" ✓ Found {len(monthly_records)} monthly records") for month_data in monthly_records: # 월 추출 month_str = month_data.get('DATE', '') if not month_str: continue if is_split: # 1,2호기: PW1, PW2 분리 inv1_gen = safe_float(month_data.get('PW1', 0)) inv2_gen = safe_float(month_data.get('PW2', 0)) results.append({ 'plant_id': 'nrems-01', 'month': month_str, 'generation_kwh': inv1_gen }) results.append({ 'plant_id': 'nrems-02', 'month': month_str, 'generation_kwh': inv2_gen }) print(f" ✓ {month_str}: Unit1={inv1_gen}kWh, Unit2={inv2_gen}kWh") else: # 3,4,9호기: INV 단일값 generation_kwh = safe_float(month_data.get('INV', 0)) results.append({ 'plant_id': plant_id, 'month': month_str, 'generation_kwh': generation_kwh }) print(f" ✓ {month_str}: {generation_kwh}kWh") print(f" → Collected {len(monthly_records)} records") else: print(f" ⚠ No monthly data found") else: print(f" ✗ HTTP {response.status_code}") except Exception as e: print(f" ✗ Error: {e}") print(f"\n[Total] Collected {len(results)} monthly records\n") return results