176 lines
6.6 KiB
Python
176 lines
6.6 KiB
Python
# ==========================================
|
|
# main.py - 태양광 발전 통합 관제 시스템
|
|
# ==========================================
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
# 환경 변수 로드 (최상단에서 실행)
|
|
try:
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
print("✅ 환경 변수 로드 완료")
|
|
except ImportError:
|
|
print("⚠️ python-dotenv가 설치되지 않았습니다. 환경 변수를 직접 설정하세요.")
|
|
|
|
from config import get_all_plants
|
|
from database import save_to_supabase, save_to_console
|
|
from crawlers import get_crawler
|
|
from crawler_manager import CrawlerManager
|
|
from alert_manager import AlertManager
|
|
|
|
# 스마트 스케줄러 초기화
|
|
crawler_manager = CrawlerManager()
|
|
|
|
def extract_unit_number(name):
|
|
"""발전소 이름에서 호기 번호 추출 (정렬용)"""
|
|
match = re.search(r'(\d+)호기', name)
|
|
if match:
|
|
return int(match.group(1))
|
|
return 999
|
|
|
|
def integrated_monitoring(save_to_db=True, company_filter=None, force_run=False):
|
|
"""
|
|
통합 모니터링 실행
|
|
|
|
Args:
|
|
save_to_db: True면 Supabase에 저장
|
|
company_filter: 특정 업체만 필터링 (예: 'sunwind')
|
|
force_run: True면 스케줄러 무시하고 강제 실행
|
|
"""
|
|
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
print(f"\n🚀 [통합 관제 시스템] 데이터 수집 시작... ({now_str})")
|
|
print("-" * 75)
|
|
|
|
# 평탄화된 발전소 목록 가져오기
|
|
all_plants = get_all_plants()
|
|
|
|
# 업체 필터링 (옵션)
|
|
if company_filter:
|
|
all_plants = [p for p in all_plants if p['company_id'] == company_filter]
|
|
print(f"📌 필터 적용: {company_filter}")
|
|
|
|
total_results = []
|
|
skipped_count = 0
|
|
|
|
# 알림 매니저 초기화
|
|
alert_manager = AlertManager()
|
|
|
|
for plant in all_plants:
|
|
plant_type = plant['type']
|
|
plant_name = plant.get('display_name', plant.get('name', 'Unknown'))
|
|
company_id = plant.get('company_id', '')
|
|
company_name = plant.get('company_name', '')
|
|
|
|
# 크롤링 결과에서 생성되는 site_id 목록 (1,2호기 분리 처리 고려)
|
|
is_split = plant.get('options', {}).get('is_split', False)
|
|
if is_split:
|
|
site_ids = ['nrems-01', 'nrems-02']
|
|
else:
|
|
site_ids = [plant.get('id', '')]
|
|
|
|
# 야간 시간대 체크 (force_run이 아닌 경우)
|
|
if not force_run:
|
|
# 대표 site_id 하나로 야간 여부 확인 (모든 사이트 동일 조건)
|
|
representative_id = site_ids[0] if site_ids else ''
|
|
if representative_id and not crawler_manager.should_run(representative_id):
|
|
print(f" ⏭️ [{plant_type.upper()}] {plant_name} 스킵 (야간 시간대)")
|
|
skipped_count += 1
|
|
continue
|
|
|
|
print(f"📡 [{plant_type.upper()}] {company_name} - {plant_name} 수집 중...")
|
|
|
|
try:
|
|
crawler_func = get_crawler(plant_type)
|
|
if crawler_func:
|
|
data = crawler_func(plant)
|
|
if data:
|
|
# company_id, company_name 주입 + 변경 여부 판단
|
|
for item in data:
|
|
item['company_id'] = company_id
|
|
item['company_name'] = company_name
|
|
item['_data_changed'] = False # 기본값: 저장 안 함
|
|
|
|
item_id = item.get('id', '')
|
|
|
|
# 알림은 항상 체크 (0kW 감지 목적)
|
|
alert_info = plant.copy()
|
|
alert_info['id'] = item_id
|
|
alert_info['name'] = item.get('name', plant_name)
|
|
alert_manager.check_and_alert(alert_info, item.get('kw', 0))
|
|
|
|
if item_id:
|
|
# 크롤링 성공 기록 (항상)
|
|
crawler_manager.record_run(item_id)
|
|
|
|
# 데이터 변경 여부 확인
|
|
# → 원격 서버가 실제로 업데이트했는지 감지
|
|
# → True면 DB 저장 대상 / False면 중복 저장 방지
|
|
if crawler_manager.check_data_change(item_id, item):
|
|
crawler_manager.analyze_and_optimize(item_id)
|
|
item['_data_changed'] = True
|
|
else:
|
|
print(f" ⏸️ [{item_id}] 데이터 변경 없음, DB 저장 스킵")
|
|
|
|
# 변경된 항목만 DB 저장 대상에 포함
|
|
for item in data:
|
|
if item.pop('_data_changed', False):
|
|
total_results.append(item)
|
|
else:
|
|
print(f" ⚠️ 알 수 없는 크롤러 타입: {plant_type}")
|
|
except Exception as e:
|
|
print(f" ❌ {plant_name} 실패: {e}")
|
|
|
|
# 정렬 (호기 번호 순)
|
|
total_results.sort(key=lambda x: extract_unit_number(x['name']))
|
|
|
|
# 중복 제거 (company_id + id 조합)
|
|
seen_keys = set()
|
|
unique_results = []
|
|
for item in total_results:
|
|
unique_key = f"{item.get('company_id', '')}_{item.get('id', '')}"
|
|
if unique_key not in seen_keys:
|
|
seen_keys.add(unique_key)
|
|
unique_results.append(item)
|
|
total_results = unique_results
|
|
|
|
print("-" * 75)
|
|
|
|
if skipped_count > 0:
|
|
print(f"📊 스킵된 사이트: {skipped_count}개 (야간 시간대)")
|
|
|
|
if total_results:
|
|
# 콘솔 출력
|
|
save_to_console(total_results)
|
|
|
|
# DB 저장
|
|
if save_to_db:
|
|
save_to_supabase(total_results)
|
|
|
|
# 이상 감지 로직
|
|
current_hour = datetime.now().hour
|
|
if 10 <= current_hour <= 17:
|
|
issues = [d['name'] for d in total_results if d.get('kw', 0) == 0]
|
|
if issues:
|
|
print("\n🚨 [이상 감지 리포트]")
|
|
for name in issues:
|
|
print(f" ⚠️ 경고: '{name}' 발전량이 0입니다! 확인 필요.")
|
|
else:
|
|
print("\n ✅ 현재 모든 발전소가 정상 가동 중입니다.")
|
|
else:
|
|
print("❌ 수집된 데이터가 없습니다.")
|
|
|
|
return total_results
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
# 인자 처리: --force 옵션으로 스케줄러 무시
|
|
force_run = '--force' in sys.argv or '-f' in sys.argv
|
|
|
|
if force_run:
|
|
print("⚡ [강제 실행 모드] 스케줄러 무시하고 모든 사이트 크롤링")
|
|
|
|
integrated_monitoring(save_to_db=True, force_run=force_run)
|
|
|