solorpower/crawler/main.py
haneulai e9a4f8e401 Add 'crawler/' from commit 'c9ed91d885f3c46efb98e98310a1882bb5936546'
git-subtree-dir: crawler
git-subtree-mainline: 8991b92ac8
git-subtree-split: c9ed91d885
2026-06-15 12:53:36 +09:00

204 lines
7.8 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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