490 lines
17 KiB
Python
490 lines
17 KiB
Python
# ==========================================
|
|
# crawlers/hyundai.py - 현대 크롤러 (8호기)
|
|
# ==========================================
|
|
|
|
import requests
|
|
from .base import create_session
|
|
|
|
def fetch_data(plant_info):
|
|
"""
|
|
현대 발전소 데이터 수집 (Hi-Smart 3.0)
|
|
"""
|
|
plant_id = plant_info.get('id', 'hyundai-08')
|
|
auth = plant_info.get('auth', {})
|
|
system = plant_info.get('system', {})
|
|
company_name = plant_info.get('company_name', '태양과바람')
|
|
plant_name = plant_info.get('name', '8호기')
|
|
|
|
user_id = auth.get('user_id', '')
|
|
password = auth.get('password', '')
|
|
site_id = auth.get('site_id', '')
|
|
|
|
base_url = system.get('base_url', '')
|
|
login_path = system.get('login_path', '')
|
|
data_path = system.get('data_path', '')
|
|
|
|
session = create_session()
|
|
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
'Origin': base_url,
|
|
'Referer': f'{base_url}/',
|
|
'X-ApiVersion': 'v1.0',
|
|
'X-App': 'HIWAY4VUETIFY',
|
|
'X-CallType': '0',
|
|
'X-Channel': 'WEB_PC',
|
|
'X-Lang': 'ko',
|
|
'X-Mid': 'login',
|
|
'X-VName': 'UI'
|
|
}
|
|
|
|
# 로그인
|
|
login_urls = [
|
|
f"{base_url}{login_path}",
|
|
f"{base_url}{login_path}.json",
|
|
f"{base_url}{login_path}.do"
|
|
]
|
|
|
|
login_success = False
|
|
|
|
for url in login_urls:
|
|
try:
|
|
payload = {"user_id": user_id, "password": password}
|
|
res = session.post(url, json=payload, headers=headers)
|
|
|
|
if res.status_code == 200:
|
|
auth_token = res.headers.get('x-auth-token')
|
|
if auth_token:
|
|
headers['x-auth-token'] = auth_token
|
|
print(f" [현대] 로그인 성공 & 토큰 확보!")
|
|
login_success = True
|
|
break
|
|
|
|
except Exception:
|
|
continue
|
|
|
|
if not login_success:
|
|
print(f"❌ 현대 {plant_name} 로그인 실패")
|
|
return []
|
|
|
|
# 데이터 요청
|
|
try:
|
|
data_url = f"{base_url}{data_path}"
|
|
params = {'site_id': site_id}
|
|
|
|
# 데이터 요청용 헤더 업데이트
|
|
headers['X-Channel'] = 'WEB_PCWeb'
|
|
headers['X-Mid'] = 'siteWork'
|
|
|
|
res = session.get(data_url, params=params, headers=headers)
|
|
|
|
if res.status_code != 200:
|
|
print(f"❌ 현대 데이터 요청 실패 (코드: {res.status_code})")
|
|
return []
|
|
|
|
data = res.json()
|
|
|
|
if 'datas' in data and 'unitedSiteInfo' in data['datas']:
|
|
info = data['datas']['unitedSiteInfo']
|
|
|
|
curr_kw = float(info.get('PVPCS_Pac', '0').replace(',', ''))
|
|
today_kwh = float(info.get('PVPCS_Daily_P', '0').replace(',', ''))
|
|
|
|
print(f" [현대] {plant_name} 데이터: {curr_kw}kW / {today_kwh}kWh")
|
|
return [{
|
|
'id': plant_id,
|
|
'name': f'{company_name} {plant_name}',
|
|
'kw': curr_kw,
|
|
'today': today_kwh,
|
|
'status': "🟢 정상" if curr_kw > 0 else "💤 대기"
|
|
}]
|
|
else:
|
|
print(f"⚠️ 현대 데이터 구조가 다릅니다.")
|
|
return []
|
|
|
|
except Exception as e:
|
|
print(f"❌ 현대 파싱 에러: {e}")
|
|
return []
|
|
|
|
|
|
def fetch_history_hourly(plant_info, start_date, end_date):
|
|
"""
|
|
현대 발전소의 시간대별 과거 데이터 수집
|
|
|
|
Args:
|
|
plant_info: {
|
|
'id': 'hyundai-08',
|
|
'name': '8호기',
|
|
'type': 'hyundai',
|
|
'auth': {'user_id': '...', 'password': '...', 'site_id': '...'},
|
|
'system': {'base_url': '...', 'login_path': '...', 'data_path': '...'},
|
|
'company_name': '태양과바람'
|
|
}
|
|
start_date: str, 시작일 (YYYY-MM-DD)
|
|
end_date: str, 종료일 (YYYY-MM-DD)
|
|
|
|
Returns:
|
|
list: [{
|
|
'plant_id': 'hyundai-08',
|
|
'timestamp': '2026-01-15 14:00:00',
|
|
'generation_kwh': 123.5,
|
|
'current_kw': 15.2
|
|
}, ...]
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from .base import safe_float
|
|
|
|
results = []
|
|
|
|
# 설정 추출
|
|
plant_id = plant_info.get('id', 'hyundai-08')
|
|
auth = plant_info.get('auth', {})
|
|
system = plant_info.get('system', {})
|
|
plant_name = plant_info.get('name', '8호기')
|
|
|
|
user_id = auth.get('user_id', '')
|
|
password = auth.get('password', '')
|
|
site_id = auth.get('site_id', '')
|
|
|
|
base_url = system.get('base_url', '')
|
|
login_path = system.get('login_path', '')
|
|
|
|
session = create_session()
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"[Hyundai History] {plant_name} ({start_date} ~ {end_date})")
|
|
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/json;charset=UTF-8',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
'Origin': base_url,
|
|
'Referer': f'{base_url}/',
|
|
'X-ApiVersion': 'v1.0',
|
|
'X-App': 'HIWAY4VUETIFY',
|
|
'X-CallType': '0',
|
|
'X-Channel': 'WEB_PC',
|
|
'X-Lang': 'ko',
|
|
'X-Mid': 'login',
|
|
'X-VName': 'UI'
|
|
}
|
|
|
|
login_urls = [
|
|
f"{base_url}{login_path}",
|
|
f"{base_url}{login_path}.json",
|
|
f"{base_url}{login_path}.do"
|
|
]
|
|
|
|
login_success = False
|
|
for url in login_urls:
|
|
try:
|
|
payload = {"user_id": user_id, "password": password}
|
|
res = session.post(url, json=payload, headers=headers)
|
|
|
|
if res.status_code == 200:
|
|
auth_token = res.headers.get('x-auth-token')
|
|
if auth_token:
|
|
headers['x-auth-token'] = auth_token
|
|
print(f" ✓ Login successful")
|
|
login_success = True
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if not login_success:
|
|
print(f" ✗ Login failed")
|
|
return results
|
|
|
|
# 날짜 범위 반복
|
|
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
|
|
headers['X-Mid'] = 'siteWork'
|
|
|
|
while current_date <= end_dt:
|
|
date_str = current_date.strftime('%Y-%m-%d')
|
|
|
|
print(f"\n[Processing Date] {date_str}")
|
|
|
|
# getSolraDayWork 엔드포인트 사용 (20분 간격 데이터)
|
|
url = f"{base_url}/hismart/site/getSolraDayWork"
|
|
params = {
|
|
'site_id': site_id,
|
|
'startDate': date_str # YYYY-MM-DD 형식
|
|
}
|
|
|
|
try:
|
|
res = session.get(url, params=params, headers=headers, timeout=10)
|
|
|
|
if res.status_code == 200:
|
|
data = res.json()
|
|
|
|
# solraDayWork 구조 파싱
|
|
day_work = data.get('datas', {}).get('solraDayWork', {})
|
|
run_data = day_work.get('runData', [])
|
|
run_time = day_work.get('runTime', [])
|
|
|
|
if run_data and run_time and len(run_data) == len(run_time):
|
|
print(f" ✓ Found {len(run_data)} records (20-min intervals)")
|
|
|
|
# runData와 runTime을 조합하여 시간대별 데이터 생성
|
|
for i in range(len(run_data)):
|
|
time_str = run_time[i] # "14:20" 형식
|
|
generation_kw = safe_float(run_data[i]) # kW 값
|
|
|
|
# timestamp 생성
|
|
timestamp = f"{date_str} {time_str}:00"
|
|
|
|
# 20분 간격 데이터를 그대로 저장 (또는 시간 단위로 집계 가능)
|
|
results.append({
|
|
'plant_id': plant_id,
|
|
'timestamp': timestamp,
|
|
'generation_kwh': generation_kw, # 실제로는 순간 kW값
|
|
'current_kw': generation_kw
|
|
})
|
|
|
|
print(f" → Collected {len(run_data)} records")
|
|
else:
|
|
print(f" ⚠ No data for {date_str}")
|
|
else:
|
|
print(f" ✗ HTTP {res.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)} records")
|
|
print(f"{'='*60}\n")
|
|
|
|
return results
|
|
|
|
|
|
def fetch_history_daily(plant_info, start_date, end_date):
|
|
"""
|
|
현대 발전소의 일별 과거 데이터 수집 (월 단위 최적화)
|
|
getSolraMonthWork API를 사용하여 한 달치 일별 데이터를 한 번에 가져옴
|
|
"""
|
|
from datetime import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
from .base import safe_float
|
|
import calendar
|
|
|
|
results = []
|
|
plant_id = plant_info.get('id', 'hyundai-08')
|
|
auth = plant_info.get('auth', {})
|
|
system = plant_info.get('system', {})
|
|
plant_name = plant_info.get('name', '8호기')
|
|
|
|
user_id = auth.get('user_id', '')
|
|
password = auth.get('password', '')
|
|
site_id = auth.get('site_id', '')
|
|
base_url = system.get('base_url', '')
|
|
login_path = system.get('login_path', '')
|
|
|
|
session = create_session()
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"[Hyundai Daily] {plant_name} ({start_date} ~ {end_date}) - Looping by Month")
|
|
print(f"{'='*60}")
|
|
|
|
# 로그인
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0',
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
'X-ApiVersion': 'v1.0',
|
|
'X-App': 'HIWAY4VUETIFY',
|
|
'X-Channel': 'WEB_PC',
|
|
'X-Lang': 'ko',
|
|
'X-Mid': 'login',
|
|
'X-VName': 'UI'
|
|
}
|
|
|
|
login_url = f"{base_url}{login_path}"
|
|
payload = {"user_id": user_id, "password": password}
|
|
try:
|
|
res = session.post(login_url, json=payload, headers=headers)
|
|
auth_token = res.headers.get('x-auth-token')
|
|
|
|
if not auth_token:
|
|
print(" ✗ Login failed")
|
|
return results
|
|
|
|
headers['x-auth-token'] = auth_token
|
|
headers['X-Mid'] = 'siteWork'
|
|
print(" ✓ Login successful")
|
|
except Exception as e:
|
|
print(f" ✗ Login error: {e}")
|
|
return results
|
|
|
|
# 월 단위 반복
|
|
current_month = datetime.strptime(start_date[:7], '%Y-%m') # YYYY-MM-01
|
|
end_month_dt = datetime.strptime(end_date[:7], '%Y-%m')
|
|
|
|
while current_month <= end_month_dt:
|
|
month_str = current_month.strftime('%Y-%m')
|
|
year = current_month.year
|
|
month = current_month.month
|
|
|
|
print(f" [Fetching] {month_str} ...", end="", flush=True)
|
|
|
|
url = f"{base_url}/hismart/site/getSolraMonthWork"
|
|
params = {'site_id': site_id, 'month': month_str}
|
|
|
|
try:
|
|
res = session.get(url, params=params, headers=headers, timeout=10)
|
|
|
|
if res.status_code == 200:
|
|
data = res.json()
|
|
day_work = data.get('datas', {}).get('solraMonthWork', {})
|
|
run_data = day_work.get('runData', [])
|
|
|
|
if run_data:
|
|
count = 0
|
|
for day_idx, val in enumerate(run_data):
|
|
day = day_idx + 1
|
|
daily_total = safe_float(val)
|
|
|
|
# 유효한 날짜인지 확인 (예: 2월 30일 방지)
|
|
try:
|
|
# 해당 월의 마지막 날짜 확인
|
|
last_day = calendar.monthrange(year, month)[1]
|
|
if day > last_day:
|
|
continue
|
|
|
|
date_str = f"{year}-{month:02d}-{day:02d}"
|
|
|
|
# 요청된 날짜 범위 내인지 확인
|
|
if date_str >= start_date and date_str <= end_date:
|
|
results.append({
|
|
'plant_id': plant_id,
|
|
'date': date_str,
|
|
'generation_kwh': round(daily_total, 2)
|
|
})
|
|
count += 1
|
|
except ValueError:
|
|
continue
|
|
|
|
print(f" OK ({count} days)")
|
|
else:
|
|
print(f" No data")
|
|
else:
|
|
print(f" HTTP {res.status_code}")
|
|
|
|
except Exception as e:
|
|
print(f" Error: {e}")
|
|
|
|
current_month += relativedelta(months=1)
|
|
|
|
print(f"\n[Total] Collected {len(results)} daily records\n")
|
|
return results
|
|
|
|
|
|
def fetch_history_monthly(plant_info, start_month, end_month):
|
|
"""
|
|
현대 발전소의 월별 과거 데이터 수집
|
|
|
|
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
|
|
from dateutil.relativedelta import relativedelta
|
|
from .base import safe_float
|
|
|
|
results = []
|
|
plant_id = plant_info.get('id', 'hyundai-08')
|
|
auth = plant_info.get('auth', {})
|
|
system = plant_info.get('system', {})
|
|
plant_name = plant_info.get('name', '8호기')
|
|
|
|
user_id = auth.get('user_id', '')
|
|
password = auth.get('password', '')
|
|
site_id = auth.get('site_id', '')
|
|
base_url = system.get('base_url', '')
|
|
login_path = system.get('login_path', '')
|
|
|
|
session = create_session()
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"[Hyundai Monthly] {plant_name} ({start_month} ~ {end_month})")
|
|
print(f"{'='*60}")
|
|
|
|
# 로그인
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0',
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
'X-ApiVersion': 'v1.0',
|
|
'X-App': 'HIWAY4VUETIFY',
|
|
'X-Channel': 'WEB_PC',
|
|
'X-Lang': 'ko',
|
|
'X-Mid': 'login',
|
|
'X-VName': 'UI'
|
|
}
|
|
|
|
login_url = f"{base_url}{login_path}"
|
|
payload = {"user_id": user_id, "password": password}
|
|
res = session.post(login_url, json=payload, headers=headers)
|
|
auth_token = res.headers.get('x-auth-token')
|
|
|
|
if not auth_token:
|
|
print(" ✗ Login failed")
|
|
return results
|
|
|
|
headers['x-auth-token'] = auth_token
|
|
headers['X-Mid'] = 'siteWork'
|
|
print(" ✓ Login successful")
|
|
|
|
current_month = datetime.strptime(start_month, '%Y-%m')
|
|
end_month_dt = datetime.strptime(end_month, '%Y-%m')
|
|
|
|
while current_month <= end_month_dt:
|
|
month_str = current_month.strftime('%Y-%m')
|
|
|
|
try:
|
|
# 실제 확인된 월별 엔드포인트: getSolraMonthWork
|
|
url = f"{base_url}/hismart/site/getSolraMonthWork"
|
|
params = {
|
|
'site_id': site_id,
|
|
'month': month_str # YYYY-MM 형식
|
|
}
|
|
|
|
res = session.get(url, params=params, headers=headers, verify=False, timeout=10)
|
|
|
|
if res.status_code == 200:
|
|
data = res.json()
|
|
|
|
# 응답 구조: datas.solraMonthWork.runData = 일별 발전량 배열
|
|
if 'datas' in data and 'solraMonthWork' in data['datas']:
|
|
month_data = data['datas']['solraMonthWork']
|
|
run_data = month_data.get('runData', [])
|
|
|
|
# runData는 해당 월의 일별 발전량 배열 → 합산
|
|
monthly_kwh = sum(run_data) if run_data else 0.0
|
|
|
|
print(f" ✓ {month_str}: {monthly_kwh:.1f}kWh (from {len(run_data)} days)")
|
|
|
|
results.append({
|
|
'plant_id': plant_id,
|
|
'month': month_str,
|
|
'generation_kwh': monthly_kwh
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f" ✗ {month_str}: {e}")
|
|
|
|
current_month += relativedelta(months=1)
|
|
|
|
print(f"[Total] Collected {len(results)} monthly records\n")
|
|
return results
|