solorpower_crawler/crawlers/nrems.py

619 lines
24 KiB
Python

# ==========================================
# 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