git-subtree-dir: crawler git-subtree-mainline:8991b92ac8git-subtree-split:c9ed91d885
204 lines
7.8 KiB
Python
204 lines
7.8 KiB
Python
# ==========================================
|
||
# main.py - 태양광 발전 통합 관제 시스템
|
||
# ==========================================
|
||
|
||
import re
|
||
from datetime import datetime, timezone, timedelta
|
||
|
||
# 환경 변수 로드 (최상단에서 실행)
|
||
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
|
||
|
||
|
||
def run_daily_close(force=False):
|
||
"""
|
||
일일 마감 집계 실행 (KST 21:00~21:10 자동 트리거 또는 force=True)
|
||
solar_logs 데이터를 집계하여 daily_stats에 당일 최종값을 확정합니다.
|
||
"""
|
||
kst = timezone(timedelta(hours=9))
|
||
kst_now = datetime.now(kst)
|
||
kst_hour = kst_now.hour
|
||
kst_minute = kst_now.minute
|
||
|
||
is_close_window = (kst_hour == 21 and kst_minute < 10)
|
||
|
||
if force or is_close_window:
|
||
date_str = kst_now.strftime('%Y-%m-%d')
|
||
print(f"\n🌙 [KST {kst_hour:02d}:{kst_minute:02d}] 일일 마감 집계 트리거 → {date_str} 통계 확정")
|
||
try:
|
||
from daily_summary import calculate_daily_stats
|
||
calculate_daily_stats(date_str)
|
||
except Exception as e:
|
||
print(f" ❌ 마감 집계 실패: {e}")
|
||
else:
|
||
print(f" ℹ️ 마감 집계 스킵 (KST {kst_hour:02d}시 {kst_minute:02d}분, 대상 시간대 아님)")
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
|
||
# 인자 처리: --force 옵션으로 스케줄러 무시
|
||
force_run = '--force' in sys.argv or '-f' in sys.argv
|
||
force_close = '--close' in sys.argv # 마감 집계 강제 실행
|
||
|
||
if force_run:
|
||
print("⚡ [강제 실행 모드] 스케줄러 무시하고 모든 사이트 크롤링")
|
||
|
||
integrated_monitoring(save_to_db=True, force_run=force_run)
|
||
|
||
# 마감 집계: 21:00~21:10 KST 자동 실행 또는 --close 옵션
|
||
run_daily_close(force=force_close)
|
||
|