refactor: complete repository structure cleanup
Some checks are pending
CI / Crawler (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Crawler (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / API (Python 3.11) (push) Waiting to run
CI / Database migration (push) Waiting to run
CI / App web build (Node 20) (push) Waiting to run
Some checks are pending
CI / Crawler (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Crawler (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / API (Python 3.11) (push) Waiting to run
CI / Database migration (push) Waiting to run
CI / App web build (Node 20) (push) Waiting to run
This commit is contained in:
parent
76172bb22a
commit
f2bb131884
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -20,8 +20,12 @@ env/
|
||||||
*.db-journal
|
*.db-journal
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
!.env.example
|
||||||
secrets.json
|
secrets.json
|
||||||
|
|
||||||
|
# Runtime output
|
||||||
|
*.log
|
||||||
|
|
||||||
# IDEs and Editors
|
# IDEs and Editors
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
12
AGENTS.md
12
AGENTS.md
|
|
@ -43,19 +43,17 @@ SolarPower is an integrated solar power monitoring system consisting of:
|
||||||
- **Run (Prod)**: Managed via systemd: `sudo systemctl restart solar-api`
|
- **Run (Prod)**: Managed via systemd: `sudo systemctl restart solar-api`
|
||||||
- **Docs**: Access Swagger UI at `http://localhost:8000/docs`
|
- **Docs**: Access Swagger UI at `http://localhost:8000/docs`
|
||||||
- **Test**:
|
- **Test**:
|
||||||
- No automated test suite (pytest) currently.
|
- Run `python -m unittest discover -s tests -p "test_*.py" -v`.
|
||||||
- Verify endpoints manually using `curl` or Swagger UI.
|
- Verify deployed endpoints using `curl` or Swagger UI.
|
||||||
|
|
||||||
### 🐍 Crawler (`crawler/`)
|
### 🐍 Crawler (`crawler/`)
|
||||||
- **Environment**: Python 3.10+
|
- **Environment**: Python 3.10+
|
||||||
- **Install**: Manual dependency installation (requests, python-dotenv, supabase).
|
- **Install**: `pip install -r requirements.txt`
|
||||||
- **Run (Manual)**: `python main.py`
|
- **Run (Manual)**: `python main.py`
|
||||||
- Runs in `LEARNING` or `OPTIMIZED` mode based on `crawler_manager.db`.
|
- Runs in `LEARNING` or `OPTIMIZED` mode based on `crawler_manager.db`.
|
||||||
- **Run (Force)**: `python main.py --force` (Ignores scheduler constraints)
|
- **Run (Force)**: `python main.py --force` (Ignores scheduler constraints)
|
||||||
- **GUI Tool**: `python crawler_gui.py` (For historical data recovery and log cleaning)
|
- **GUI Tool**: `python crawler_gui.py` (For historical data recovery and log cleaning)
|
||||||
- **Testing**:
|
- **Testing**: `python -m unittest discover -s tests -p "test_*.py" -v`
|
||||||
- No standard test runner. Use standalone scripts in `tests/`.
|
|
||||||
- **Run Single Test**: `python tests/debug_kremc.py` (or similar script for specific site).
|
|
||||||
|
|
||||||
## 4. Code Style & Standards
|
## 4. Code Style & Standards
|
||||||
|
|
||||||
|
|
@ -99,7 +97,7 @@ SolarPower is an integrated solar power monitoring system consisting of:
|
||||||
### ☁️ Server Deployment
|
### ☁️ Server Deployment
|
||||||
1. **Push**: Commit and push changes to the remote Git repository.
|
1. **Push**: Commit and push changes to the remote Git repository.
|
||||||
2. **Pull**: SSH into the server and pull changes.
|
2. **Pull**: SSH into the server and pull changes.
|
||||||
- Command: `ssh user@server "cd ~/solorpower_server && git pull"`
|
- Command: `ssh user@server "cd ~/solorpower && git pull"`
|
||||||
3. **Restart**: Restart the API service.
|
3. **Restart**: Restart the API service.
|
||||||
- Command: `sudo systemctl restart solar-api`
|
- Command: `sudo systemctl restart solar-api`
|
||||||
|
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
태양광 발전소의 실시간 발전 데이터를 수집하고 모니터링하는 통합 관제 시스템입니다.
|
태양광 발전소의 실시간 발전 데이터를 수집하고 모니터링하는 통합 관제 시스템입니다.
|
||||||
|
|
||||||
|
개발 환경은 [개발 및 테스트 가이드](docs/development_and_testing.md), 디렉터리 책임과 생성 파일 정책은 [저장소 구조 문서](docs/repository_structure.md)를 기준으로 합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 목차
|
## 📋 목차
|
||||||
|
|
@ -114,7 +116,7 @@ flowchart TD
|
||||||
### 위치
|
### 위치
|
||||||
```
|
```
|
||||||
d:\dev\etc\SolorPower\crawler\ (Local PC)
|
d:\dev\etc\SolorPower\crawler\ (Local PC)
|
||||||
/home/ubuntu/SolorPower/crawler/ (Oracle Cloud)
|
/home/ubuntu/solorpower/crawler/ (Oracle Cloud)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 네트워크 구성 (Tailscale)
|
### 네트워크 구성 (Tailscale)
|
||||||
|
|
@ -157,7 +159,7 @@ crawler/
|
||||||
### 자동 수집 (Cron)
|
### 자동 수집 (Cron)
|
||||||
```bash
|
```bash
|
||||||
# 30분마다 실행 (Oracle Cloud 환경)
|
# 30분마다 실행 (Oracle Cloud 환경)
|
||||||
*/30 * * * * cd /home/ubuntu/SolorPower/crawler && /home/ubuntu/SolorPower/crawler/venv/bin/python main.py >> cron.log 2>&1
|
*/10 * * * * cd /home/ubuntu/solorpower/crawler && /usr/bin/python3 main.py >> cron.log 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 크롤러별 개선 사항 (2026.01)
|
### 크롤러별 개선 사항 (2026.01)
|
||||||
|
|
@ -173,7 +175,7 @@ crawler/
|
||||||
|
|
||||||
### 위치
|
### 위치
|
||||||
```
|
```
|
||||||
/home/ubuntu/SolorPower/api_server/
|
/home/ubuntu/solorpower/api_server/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 환경 설정 (.env)
|
### 환경 설정 (.env)
|
||||||
|
|
@ -219,6 +221,8 @@ d:\dev\etc\SolorPower\app\
|
||||||
- 별칭(`발전량`, `kwh` 등) 및 셀 병합 처리 지원.
|
- 별칭(`발전량`, `kwh` 등) 및 셀 병합 처리 지원.
|
||||||
- **반응형 웹 지원**: PC/모바일 브라우저 최적화.
|
- **반응형 웹 지원**: PC/모바일 브라우저 최적화.
|
||||||
|
|
||||||
|
API 주소와 조회 업체는 `EXPO_PUBLIC_API_BASE_URL`, `EXPO_PUBLIC_COMPANY_ID`로 설정합니다. 예시는 `app/.env.example`을 참고하며 공개 설정 파일에 인증정보를 넣지 않습니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 웹 앱 배포 가이드 (Web Deployment)
|
## 웹 앱 배포 가이드 (Web Deployment)
|
||||||
|
|
@ -228,7 +232,8 @@ React Native(Expo) 앱을 웹으로 빌드하고 Nginx 서버에 배포하는
|
||||||
### 1. 웹 빌드 (Local PC)
|
### 1. 웹 빌드 (Local PC)
|
||||||
```bash
|
```bash
|
||||||
cd d:\dev\etc\SolorPower\app
|
cd d:\dev\etc\SolorPower\app
|
||||||
npm run export:web
|
npm ci
|
||||||
|
npm run build:web
|
||||||
```
|
```
|
||||||
- `dist` 폴더에 정적 웹 파일들이 생성됩니다.
|
- `dist` 폴더에 정적 웹 파일들이 생성됩니다.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||||
from supabase import Client
|
from supabase import Client
|
||||||
from typing import Literal, Optional
|
from typing import Annotated, Literal, Optional
|
||||||
from datetime import date as date_type
|
from datetime import date as date_type
|
||||||
import calendar
|
import calendar
|
||||||
import re
|
import re
|
||||||
|
|
@ -63,7 +63,10 @@ def get_all_plants_comparison(
|
||||||
date: Optional[str] = Query(None, description="날짜 (YYYY-MM-DD)"),
|
date: Optional[str] = Query(None, description="날짜 (YYYY-MM-DD)"),
|
||||||
year: Optional[int] = Query(None, ge=MIN_STATS_YEAR, le=MAX_STATS_YEAR, description="연도"),
|
year: Optional[int] = Query(None, ge=MIN_STATS_YEAR, le=MAX_STATS_YEAR, description="연도"),
|
||||||
month: Optional[int] = Query(None, ge=1, le=12, description="월"),
|
month: Optional[int] = Query(None, ge=1, le=12, description="월"),
|
||||||
db: Client = Depends(get_db)
|
db: Client = Depends(get_db),
|
||||||
|
company_id: Annotated[
|
||||||
|
Optional[int], Query(ge=1, description="업체 ID")
|
||||||
|
] = None,
|
||||||
) -> ComparisonStatsResponse:
|
) -> ComparisonStatsResponse:
|
||||||
"""
|
"""
|
||||||
전체 발전소 발전량 비교 통계 조회
|
전체 발전소 발전량 비교 통계 조회
|
||||||
|
|
@ -75,8 +78,12 @@ def get_all_plants_comparison(
|
||||||
target_year = year if year is not None else target_date.year
|
target_year = year if year is not None else target_date.year
|
||||||
target_month = month if month is not None else target_date.month
|
target_month = month if month is not None else target_date.month
|
||||||
|
|
||||||
# 1. 모든 발전소 기본 정보 조회 (이름, 용량)
|
# 업체가 지정되면 해당 업체 발전소만 비교한다. 미지정 호출은 기존
|
||||||
plants_res = db.table("plants").select("id, name, capacity").execute()
|
# 전체 비교 계약을 유지하여 하위 호환성을 보장한다.
|
||||||
|
plants_query = db.table("plants").select("id, name, capacity")
|
||||||
|
if company_id is not None:
|
||||||
|
plants_query = plants_query.eq("company_id", company_id)
|
||||||
|
plants_res = plants_query.execute()
|
||||||
plants = {p['id']: p for p in plants_res.data}
|
plants = {p['id']: p for p in plants_res.data}
|
||||||
|
|
||||||
# 결과 초기화
|
# 결과 초기화
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,13 @@ class ApiContractTest(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(400, response.status_code)
|
self.assertEqual(400, response.status_code)
|
||||||
|
|
||||||
|
def test_invalid_comparison_company_returns_422(self):
|
||||||
|
response = self.client.get(
|
||||||
|
"/plants/stats/comparison?period=day&company_id=0"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(422, response.status_code)
|
||||||
|
|
||||||
def test_invalid_month_returns_422(self):
|
def test_invalid_month_returns_422(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/plants/nrems-03/stats?period=day&year=2026&month=13"
|
"/plants/nrems-03/stats?period=day&year=2026&month=13"
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,17 @@ class StatsTimezoneTest(unittest.TestCase):
|
||||||
self.assertIn(("lt", "created_at", "2026-01-01T15:00:00+00:00"), query.filters)
|
self.assertIn(("lt", "created_at", "2026-01-01T15:00:00+00:00"), query.filters)
|
||||||
self.assertEqual(123.4, result.data[0].generation)
|
self.assertEqual(123.4, result.data[0].generation)
|
||||||
|
|
||||||
|
def test_comparison_filters_plants_by_company_when_requested(self):
|
||||||
|
db = FakeDb()
|
||||||
|
|
||||||
|
stats.get_all_plants_comparison(
|
||||||
|
period="day", date="2026-01-01", year=None, month=None,
|
||||||
|
db=db, company_id=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
query = db.latest_query("plants")
|
||||||
|
self.assertIn(("eq", "company_id", 7), query.filters)
|
||||||
|
|
||||||
def test_plant_today_query_has_upper_bound(self):
|
def test_plant_today_query_has_upper_bound(self):
|
||||||
db = FakeDb([{"today_kwh": 77.0}])
|
db = FakeDb([{"today_kwh": 77.0}])
|
||||||
|
|
||||||
|
|
|
||||||
3
app/.env.example
Normal file
3
app/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Expo exposes EXPO_PUBLIC_* values to the app bundle. Do not put secrets here.
|
||||||
|
EXPO_PUBLIC_API_BASE_URL=https://solorpower.dadot.net
|
||||||
|
EXPO_PUBLIC_COMPANY_ID=1
|
||||||
23
app/App.js
23
app/App.js
|
|
@ -16,8 +16,9 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import PlantDetailScreen from './screens/PlantDetailScreen';
|
import PlantDetailScreen from './screens/PlantDetailScreen';
|
||||||
import ComparisonScreen from './screens/ComparisonScreen';
|
import ComparisonScreen from './screens/ComparisonScreen';
|
||||||
|
import { appConfig } from './config/appConfig';
|
||||||
|
import { getPlants, updatePlantAlerts } from './services/solarApi';
|
||||||
|
|
||||||
const API_URL = 'https://solorpower.dadot.net/plants/1';
|
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
// 메인 대시보드 화면
|
// 메인 대시보드 화면
|
||||||
|
|
@ -33,11 +34,7 @@ function DashboardScreen({ navigation }) {
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await fetch(API_URL);
|
const result = await getPlants();
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP Error: ${response.status}`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
|
@ -62,17 +59,7 @@ function DashboardScreen({ navigation }) {
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`https://solorpower.dadot.net/plants/${plant.company_id || 1}/${plant.id}/alerts`, {
|
await updatePlantAlerts(plant.company_id, plant.id, newValue);
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ alerts_enabled: newValue }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to update alert settings');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
|
|
@ -196,7 +183,7 @@ function DashboardScreen({ navigation }) {
|
||||||
<Text style={styles.headerTitle}>☀️ 태양광 발전 현황</Text>
|
<Text style={styles.headerTitle}>☀️ 태양광 발전 현황</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.compareButton}
|
style={styles.compareButton}
|
||||||
onPress={() => navigation.navigate('Comparison')}
|
onPress={() => navigation.navigate('Comparison', { companyId: appConfig.companyId })}
|
||||||
>
|
>
|
||||||
<Text style={styles.compareButtonText}>📊 전체 비교</Text>
|
<Text style={styles.compareButtonText}>📊 전체 비교</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
||||||
76
app/components/StatsPeriodControls.js
Normal file
76
app/components/StatsPeriodControls.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ key: 'today', label: '오늘' },
|
||||||
|
{ key: 'day', label: '일간' },
|
||||||
|
{ key: 'month', label: '월간' },
|
||||||
|
{ key: 'year', label: '연간' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function StatsPeriodControls({
|
||||||
|
dateLabel,
|
||||||
|
onMoveDate,
|
||||||
|
onPeriodChange,
|
||||||
|
period,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={styles.dateControl}>
|
||||||
|
<TouchableOpacity onPress={() => onMoveDate(-1)} style={styles.arrowButton}>
|
||||||
|
<Text style={styles.arrowText}>◀</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.dateText}>{dateLabel}</Text>
|
||||||
|
<TouchableOpacity onPress={() => onMoveDate(1)} style={styles.arrowButton}>
|
||||||
|
<Text style={styles.arrowText}>▶</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={styles.tabContainer}>
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={tab.key}
|
||||||
|
style={[styles.tab, period === tab.key && styles.tabActive]}
|
||||||
|
onPress={() => onPeriodChange(tab.key)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tabText, period === tab.key && styles.tabTextActive]}>
|
||||||
|
{tab.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
dateControl: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: 12,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
arrowButton: { padding: 8 },
|
||||||
|
arrowText: { fontSize: 20, color: '#4B5563' },
|
||||||
|
dateText: { fontSize: 16, fontWeight: 'bold', color: '#1F2937' },
|
||||||
|
tabContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 4,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
tabActive: { backgroundColor: '#3B82F6' },
|
||||||
|
tabText: { fontSize: 13, fontWeight: '600', color: '#6B7280' },
|
||||||
|
tabTextActive: { color: '#FFFFFF' },
|
||||||
|
});
|
||||||
|
|
@ -14,8 +14,7 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import * as DocumentPicker from 'expo-document-picker';
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
|
import { uploadGenerationData } from '../services/solarApi';
|
||||||
const API_URL = 'https://solorpower.dadot.net';
|
|
||||||
|
|
||||||
export default function UploadModal({ visible, onClose, plantId, onUploadSuccess }) {
|
export default function UploadModal({ visible, onClose, plantId, onUploadSuccess }) {
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
@ -70,29 +69,11 @@ export default function UploadModal({ visible, onClose, plantId, onUploadSuccess
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploadType에 따라 엔드포인트 분기
|
const result = await uploadGenerationData(plantId, uploadType, formData);
|
||||||
const endpoint = uploadType === 'daily'
|
Alert.alert('성공', result.message || '업로드가 완료되었습니다.');
|
||||||
? `/plants/${plantId}/upload`
|
setSelectedFile(null);
|
||||||
: `/plants/${plantId}/upload/monthly`;
|
onUploadSuccess?.();
|
||||||
|
onClose();
|
||||||
console.log(`🚀 Uploading ${uploadType} data`);
|
|
||||||
console.log(" URL:", `${API_URL}${endpoint}`);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
Alert.alert('성공', result.message || '업로드가 완료되었습니다.');
|
|
||||||
setSelectedFile(null);
|
|
||||||
onUploadSuccess?.();
|
|
||||||
onClose();
|
|
||||||
} else {
|
|
||||||
Alert.alert('오류', result.detail || '업로드에 실패했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert('오류', `업로드 중 오류가 발생했습니다: ${error.message}`);
|
Alert.alert('오류', `업로드 중 오류가 발생했습니다: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
20
app/config/appConfig.js
Normal file
20
app/config/appConfig.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
const DEFAULT_API_BASE_URL = 'https://solorpower.dadot.net';
|
||||||
|
const DEFAULT_COMPANY_ID = 1;
|
||||||
|
|
||||||
|
const normalizeBaseUrl = (value) => value.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const parseCompanyId = (value) => {
|
||||||
|
const companyId = Number.parseInt(value, 10);
|
||||||
|
return Number.isInteger(companyId) && companyId > 0
|
||||||
|
? companyId
|
||||||
|
: DEFAULT_COMPANY_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is the single tenant-selection boundary until the UI supports switching
|
||||||
|
// companies. Deployments can select another company without changing source.
|
||||||
|
export const appConfig = Object.freeze({
|
||||||
|
apiBaseUrl: normalizeBaseUrl(
|
||||||
|
process.env.EXPO_PUBLIC_API_BASE_URL || DEFAULT_API_BASE_URL,
|
||||||
|
),
|
||||||
|
companyId: parseCompanyId(process.env.EXPO_PUBLIC_COMPANY_ID),
|
||||||
|
});
|
||||||
73
app/hooks/usePlantStats.js
Normal file
73
app/hooks/usePlantStats.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { getHourlyPlantStats, getPlantStats } from '../services/solarApi';
|
||||||
|
import {
|
||||||
|
formatHourlyChartData,
|
||||||
|
formatPeriodChartData,
|
||||||
|
toLocalIsoDate,
|
||||||
|
} from '../utils/statsChart';
|
||||||
|
|
||||||
|
const buildPeriodParams = (period, selectedDate) => {
|
||||||
|
const year = selectedDate.getFullYear();
|
||||||
|
const month = selectedDate.getMonth() + 1;
|
||||||
|
|
||||||
|
if (period === 'day') return { period, year, month };
|
||||||
|
return { period, year };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUpdatedAt = (value) => {
|
||||||
|
if (!value) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '-';
|
||||||
|
return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function usePlantStats(plant, period, selectedDate) {
|
||||||
|
const [chartData, setChartData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (period === 'today') {
|
||||||
|
const result = await getHourlyPlantStats(plant.id, toLocalIsoDate(selectedDate));
|
||||||
|
setChartData(formatHourlyChartData(result?.data || [], selectedDate));
|
||||||
|
} else {
|
||||||
|
const result = await getPlantStats(
|
||||||
|
plant.id,
|
||||||
|
buildPeriodParams(period, selectedDate),
|
||||||
|
);
|
||||||
|
setChartData(formatPeriodChartData(period, result?.data || [], selectedDate));
|
||||||
|
}
|
||||||
|
} catch (requestError) {
|
||||||
|
setError(requestError.message);
|
||||||
|
setChartData([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [period, plant.id, selectedDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reload();
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const todayData = useMemo(() => {
|
||||||
|
if (period !== 'today' || toLocalIsoDate(selectedDate) !== toLocalIsoDate(new Date())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestLog = plant.latest_log;
|
||||||
|
if (!latestLog) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentKw: Number(latestLog.current_kw) || 0,
|
||||||
|
todayKwh: Number(latestLog.today_kwh) || 0,
|
||||||
|
updatedAt: formatUpdatedAt(latestLog.created_at),
|
||||||
|
};
|
||||||
|
}, [period, plant.latest_log, selectedDate]);
|
||||||
|
|
||||||
|
return { chartData, loading, error, reload, todayData };
|
||||||
|
}
|
||||||
|
|
@ -10,10 +10,10 @@ import {
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { BarChart } from 'react-native-gifted-charts';
|
import { BarChart } from 'react-native-gifted-charts';
|
||||||
|
import { appConfig } from '../config/appConfig';
|
||||||
|
import { getComparisonStats } from '../services/solarApi';
|
||||||
|
|
||||||
const API_BASE_URL = 'https://solorpower.dadot.net';
|
export default function ComparisonScreen({ navigation, route }) {
|
||||||
|
|
||||||
export default function ComparisonScreen({ navigation }) {
|
|
||||||
const [period, setPeriod] = useState('day');
|
const [period, setPeriod] = useState('day');
|
||||||
const [statsData, setStatsData] = useState([]);
|
const [statsData, setStatsData] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -58,19 +58,17 @@ export default function ComparisonScreen({ navigation }) {
|
||||||
const d = currentDate.getDate();
|
const d = currentDate.getDate();
|
||||||
const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||||
|
|
||||||
let query = `period=${period}`;
|
const query = { company_id: route.params?.companyId || appConfig.companyId, period };
|
||||||
if (period === 'day') {
|
if (period === 'day') {
|
||||||
query += `&date=${dateStr}`;
|
query.date = dateStr;
|
||||||
} else if (period === 'month') {
|
} else if (period === 'month') {
|
||||||
query += `&year=${y}&month=${m}`;
|
query.year = y;
|
||||||
|
query.month = m;
|
||||||
} else if (period === 'year') {
|
} else if (period === 'year') {
|
||||||
query += `&year=${y}`;
|
query.year = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/plants/stats/comparison?${query}`);
|
const result = await getComparisonStats(query);
|
||||||
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
setStatsData(result.data || []);
|
setStatsData(result.data || []);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -79,7 +77,7 @@ export default function ComparisonScreen({ navigation }) {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [period, currentDate]);
|
}, [period, currentDate, route.params?.companyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
|
|
@ -6,27 +6,26 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Alert,
|
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { BarChart } from 'react-native-gifted-charts';
|
import { BarChart } from 'react-native-gifted-charts';
|
||||||
|
import StatsPeriodControls from '../components/StatsPeriodControls';
|
||||||
import UploadModal from '../components/UploadModal';
|
import UploadModal from '../components/UploadModal';
|
||||||
|
import usePlantStats from '../hooks/usePlantStats';
|
||||||
const API_BASE_URL = 'https://solorpower.dadot.net';
|
|
||||||
|
|
||||||
export default function PlantDetailScreen({ route, navigation }) {
|
export default function PlantDetailScreen({ route, navigation }) {
|
||||||
const { plant } = route.params;
|
const { plant } = route.params;
|
||||||
const [period, setPeriod] = useState('today');
|
const [period, setPeriod] = useState('today');
|
||||||
const [chartData, setChartData] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [uploadVisible, setUploadVisible] = useState(false);
|
const [uploadVisible, setUploadVisible] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [todayData, setTodayData] = useState(null);
|
|
||||||
const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, label: '', value: 0 });
|
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
|
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const { chartData, loading, error, reload: fetchStats, todayData } = usePlantStats(
|
||||||
|
plant,
|
||||||
|
period,
|
||||||
|
currentDate,
|
||||||
|
);
|
||||||
|
|
||||||
// 탭 변경 시 날짜 초기화
|
// 탭 변경 시 날짜 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -51,144 +50,6 @@ export default function PlantDetailScreen({ route, navigation }) {
|
||||||
setCurrentDate(newDate);
|
setCurrentDate(newDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 통계 데이터 조회
|
|
||||||
const fetchStats = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const y = currentDate.getFullYear();
|
|
||||||
const m = currentDate.getMonth() + 1;
|
|
||||||
const d = currentDate.getDate();
|
|
||||||
const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
||||||
|
|
||||||
if (period === 'today') {
|
|
||||||
// 오늘 실시간 데이터는 이미 전달받은 plant.latest_log 사용 (상단 카드용)
|
|
||||||
// 만약 과거 날짜를 선택했다면 상단 카드는 숨기거나 해당 날짜의 요약으로 대체해야 함.
|
|
||||||
// 여기선 '오늘'인 경우에만 상단 카드를 보여주는 식으로 처리 가능.
|
|
||||||
|
|
||||||
// 서버에서 시간별 데이터 조회
|
|
||||||
const response = await fetch(`${API_BASE_URL}/plants/${plant.id}/stats/today?date=${dateStr}`);
|
|
||||||
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
|
|
||||||
const result = await response.json();
|
|
||||||
const hourlyData = result.data || [];
|
|
||||||
|
|
||||||
// 24시간 데이터 포맷팅
|
|
||||||
const formattedData = [];
|
|
||||||
for (let i = 0; i <= 24; i++) {
|
|
||||||
if (i === 24) {
|
|
||||||
formattedData.push({ value: 0, label: '24시', frontColor: 'transparent' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = hourlyData.find(d => d.hour === i) || {};
|
|
||||||
// 현재 시간인지 확인 (오늘 날짜이고 현재 시간인 경우)
|
|
||||||
const isToday = new Date().toDateString() === currentDate.toDateString();
|
|
||||||
const isCurrentHour = isToday && (i === new Date().getHours());
|
|
||||||
const hasData = item.has_data || item.current_kw > 0;
|
|
||||||
|
|
||||||
let color = '#E5E7EB';
|
|
||||||
if (isCurrentHour) {
|
|
||||||
color = '#10B981';
|
|
||||||
} else if (hasData) {
|
|
||||||
color = '#3B82F6';
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedData.push({
|
|
||||||
value: item.current_kw || 0,
|
|
||||||
label: i % 2 === 0 ? `${i}시` : '',
|
|
||||||
frontColor: color,
|
|
||||||
onPress: () => i < 24 && showTooltip(item.current_kw || 0, `${i}시`),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setChartData(formattedData);
|
|
||||||
|
|
||||||
// Today 탭이지만 과거 날짜인 경우 상단 요약 카드 데이터 갱신 필요
|
|
||||||
// API 결과에 daily total이 포함되어 있다고 가정하거나, hourly sum을 해야 함.
|
|
||||||
// 현재 API(stats/today)는 today_kwh를 주지만, 이는 그 시간대까지의 누적임.
|
|
||||||
// 마지막 시간대의 today_kwh가 그날의 총 발전량.
|
|
||||||
// 편의상 차트 데이터만 갱신.
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// 기존 일간/월간/연간 조회
|
|
||||||
// 쿼리 파라미터 추가
|
|
||||||
let query = `period=${period}`;
|
|
||||||
if (period === 'day') {
|
|
||||||
// period=day -> year, month 필요
|
|
||||||
query += `&year=${y}&month=${m}`;
|
|
||||||
} else if (period === 'month') {
|
|
||||||
// period=month -> year 필요
|
|
||||||
query += `&year=${y}`;
|
|
||||||
} else if (period === 'year') {
|
|
||||||
// period=year -> year 필요 (기준 연도)
|
|
||||||
query += `&year=${y}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/plants/${plant.id}/stats?${query}`);
|
|
||||||
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
const rawData = result.data || [];
|
|
||||||
const formattedData = [];
|
|
||||||
|
|
||||||
if (period === 'day') {
|
|
||||||
// 선택된 달의 마지막 날
|
|
||||||
const lastDay = new Date(y, m, 0).getDate();
|
|
||||||
const dataMap = {};
|
|
||||||
rawData.forEach(item => { dataMap[item.label] = item.value; });
|
|
||||||
|
|
||||||
for (let day = 1; day <= lastDay; day++) {
|
|
||||||
const ds = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
const val = dataMap[ds] || 0;
|
|
||||||
const showLabel = (day === 1 || day % 5 === 0);
|
|
||||||
|
|
||||||
formattedData.push({
|
|
||||||
value: val,
|
|
||||||
label: showLabel ? `${day}` : '',
|
|
||||||
labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -10 },
|
|
||||||
frontColor: val > 0 ? '#3B82F6' : '#E5E7EB',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (period === 'month') {
|
|
||||||
// 1월 ~ 12월
|
|
||||||
const dataMap = {};
|
|
||||||
rawData.forEach(item => { dataMap[item.label] = item.value; });
|
|
||||||
|
|
||||||
for (let month = 1; month <= 12; month++) {
|
|
||||||
const ms = `${y}-${String(month).padStart(2, '0')}`;
|
|
||||||
const val = dataMap[ms] || 0;
|
|
||||||
|
|
||||||
formattedData.push({
|
|
||||||
value: val,
|
|
||||||
label: `${month}월`,
|
|
||||||
labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -5 },
|
|
||||||
frontColor: val > 0 ? '#3B82F6' : '#E5E7EB',
|
|
||||||
renderTooltip: renderTooltip,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// year (기존 유지)
|
|
||||||
rawData.forEach(item => {
|
|
||||||
formattedData.push({
|
|
||||||
value: item.value || 0,
|
|
||||||
label: formatLabel(item.label, period),
|
|
||||||
labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -10 },
|
|
||||||
frontColor: item.value > 0 ? '#3B82F6' : '#E5E7EB',
|
|
||||||
renderTooltip: renderTooltip,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setChartData(formattedData);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
setChartData([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [plant.id, period, currentDate]);
|
|
||||||
|
|
||||||
// 날짜 포맷팅 (YYYY년 MM월 DD일 등)
|
// 날짜 포맷팅 (YYYY년 MM월 DD일 등)
|
||||||
const formatDateDisplay = () => {
|
const formatDateDisplay = () => {
|
||||||
const y = currentDate.getFullYear();
|
const y = currentDate.getFullYear();
|
||||||
|
|
@ -202,36 +63,8 @@ export default function PlantDetailScreen({ route, navigation }) {
|
||||||
return `${y}년 기준`;
|
return `${y}년 기준`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 기간 설명 (수정)
|
|
||||||
const getPeriodDescription = () => {
|
|
||||||
return formatDateDisplay();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 탭 렌더링 (아래 날짜 컨트롤 추가)
|
|
||||||
// ... renderTabs는 그대로 두고, 호출하는 곳에서 컨트롤 추가
|
|
||||||
|
|
||||||
// 날짜 컨트롤 렌더링
|
|
||||||
const renderDateControl = () => (
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginVertical: 12, gap: 16 }}>
|
|
||||||
<TouchableOpacity onPress={() => moveDate(-1)} style={{ padding: 8 }}>
|
|
||||||
<Text style={{ fontSize: 20, color: '#4B5563' }}>◀</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={{ fontSize: 16, fontWeight: 'bold', color: '#1F2937' }}>
|
|
||||||
{formatDateDisplay()}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity onPress={() => moveDate(1)} style={{ padding: 8 }}>
|
|
||||||
<Text style={{ fontSize: 20, color: '#4B5563' }}>▶</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStats();
|
|
||||||
}, [fetchStats]);
|
|
||||||
|
|
||||||
// 툴팁 표시
|
|
||||||
// 툴팁 렌더링 (커스텀)
|
// 툴팁 렌더링 (커스텀)
|
||||||
const renderTooltip = (item, index) => {
|
const renderTooltip = (item) => {
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View style={{
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
|
|
@ -248,52 +81,11 @@ export default function PlantDetailScreen({ route, navigation }) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨 포맷팅
|
|
||||||
const formatLabel = (label, period) => {
|
|
||||||
if (!label) return '';
|
|
||||||
|
|
||||||
if (period === 'day') {
|
|
||||||
const parts = label.split('-');
|
|
||||||
return parts.length >= 3 ? `${parseInt(parts[2], 10)}` : label;
|
|
||||||
} else if (period === 'month') {
|
|
||||||
const parts = label.split('-');
|
|
||||||
// 앞에 0 제거하고 '월' 추가 (예: 01 -> 1월)
|
|
||||||
return parts.length >= 2 ? `${parseInt(parts[1], 10)}월` : label;
|
|
||||||
} else {
|
|
||||||
return label.length === 4 ? `${label.slice(2)}년` : label;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 총 발전량 계산
|
// 총 발전량 계산
|
||||||
const getTotalGeneration = () => {
|
const getTotalGeneration = () => {
|
||||||
return chartData.reduce((sum, item) => sum + (item.value || 0), 0);
|
return chartData.reduce((sum, item) => sum + (item.value || 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 탭 렌더링
|
|
||||||
const renderTabs = () => (
|
|
||||||
<View style={styles.tabContainer}>
|
|
||||||
{[
|
|
||||||
{ key: 'today', label: '오늘' },
|
|
||||||
{ key: 'day', label: '일간' },
|
|
||||||
{ key: 'month', label: '월간' },
|
|
||||||
{ key: 'year', label: '연간' },
|
|
||||||
].map((tab) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={tab.key}
|
|
||||||
style={[styles.tab, period === tab.key && styles.tabActive]}
|
|
||||||
onPress={() => setPeriod(tab.key)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.tabText, period === tab.key && styles.tabTextActive]}>
|
|
||||||
{tab.label}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 기간 설명 (수정 - 위에서 이미 정의됨, 중복 제거)
|
|
||||||
// const getPeriodDescription = ... (removed)
|
|
||||||
|
|
||||||
const chartWidth = Math.min(width - 60, 600);
|
const chartWidth = Math.min(width - 60, 600);
|
||||||
|
|
||||||
// 막대 너비 및 간격 계산
|
// 막대 너비 및 간격 계산
|
||||||
|
|
@ -329,11 +121,12 @@ export default function PlantDetailScreen({ route, navigation }) {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
<ScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
||||||
{/* 날짜 컨트롤 */}
|
<StatsPeriodControls
|
||||||
{renderDateControl()}
|
dateLabel={formatDateDisplay()}
|
||||||
|
onMoveDate={moveDate}
|
||||||
{/* 탭 */}
|
onPeriodChange={setPeriod}
|
||||||
{renderTabs()}
|
period={period}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 오늘 실시간 현황 카드 (today 탭일 때만) */}
|
{/* 오늘 실시간 현황 카드 (today 탭일 때만) */}
|
||||||
{period === 'today' && todayData && (
|
{period === 'today' && todayData && (
|
||||||
|
|
@ -360,7 +153,7 @@ export default function PlantDetailScreen({ route, navigation }) {
|
||||||
<View style={styles.chartSection}>
|
<View style={styles.chartSection}>
|
||||||
<View style={styles.chartHeader}>
|
<View style={styles.chartHeader}>
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>
|
||||||
📊 {period === 'today' ? '시간대별 출력' : '발전량 추이'} ({getPeriodDescription()})
|
📊 {period === 'today' ? '시간대별 출력' : '발전량 추이'} ({formatDateDisplay()})
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.chartHint}>막대를 터치하면 상세 정보</Text>
|
<Text style={styles.chartHint}>막대를 터치하면 상세 정보</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -503,36 +296,6 @@ const styles = StyleSheet.create({
|
||||||
gap: 16,
|
gap: 16,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tabs
|
|
||||||
tabContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 4,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
tabActive: {
|
|
||||||
backgroundColor: '#3B82F6',
|
|
||||||
},
|
|
||||||
tabText: {
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#6B7280',
|
|
||||||
},
|
|
||||||
tabTextActive: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Today Card
|
// Today Card
|
||||||
todayCard: {
|
todayCard: {
|
||||||
backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
Starting up http-server, serving dist
|
|
||||||
|
|
||||||
http-server version: 14.1.1
|
|
||||||
|
|
||||||
http-server settings:
|
|
||||||
CORS: disabled
|
|
||||||
Cache: 3600 seconds
|
|
||||||
Connection Timeout: 120 seconds
|
|
||||||
Directory Listings: visible
|
|
||||||
AutoIndex: visible
|
|
||||||
Serve GZIP Files: false
|
|
||||||
Serve Brotli Files: false
|
|
||||||
Default File Extension: none
|
|
||||||
|
|
||||||
Available on:
|
|
||||||
http://172.31.160.1:3004
|
|
||||||
http://192.168.45.31:3004
|
|
||||||
http://127.0.0.1:3004
|
|
||||||
Hit CTRL-C to stop the server
|
|
||||||
|
|
||||||
[2026-02-12T01:44:51.238Z] "HEAD /" "curl/8.12.1"
|
|
||||||
(node:1368) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
|
|
||||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
||||||
[2026-02-12T01:46:00.070Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:46:00.119Z] "GET /_expo/static/js/web/AppEntry-943aad0f5c4e74ff22d7c538b45fdd54.js" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:46:00.399Z] "GET /favicon.ico" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:46:00.400Z] "GET /favicon.ico" Error (404): "Not found"
|
|
||||||
[2026-02-12T01:51:33.504Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:52:17.173Z] "HEAD /" "curl/8.12.1"
|
|
||||||
[2026-02-12T01:52:33.791Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:52:33.821Z] "GET /_expo/static/js/web/AppEntry-8dbed585399d63adcee0c00c59fe829b.js" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:52:34.014Z] "GET /favicon.ico" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
||||||
[2026-02-12T01:52:34.015Z] "GET /favicon.ico" Error (404): "Not found"
|
|
||||||
63
app/services/solarApi.js
Normal file
63
app/services/solarApi.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { appConfig } from '../config/appConfig';
|
||||||
|
|
||||||
|
const buildQuery = (params) => {
|
||||||
|
const entries = Object.entries(params)
|
||||||
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||||
|
|
||||||
|
return entries.length > 0 ? `?${entries.join('&')}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestJson = async (path, options = {}) => {
|
||||||
|
const response = await fetch(`${appConfig.apiBaseUrl}${path}`, options);
|
||||||
|
let payload = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch {
|
||||||
|
// Preserve the HTTP status when an upstream proxy returns a non-JSON body.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = payload?.detail || payload?.message || `HTTP Error: ${response.status}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlants = (companyId = appConfig.companyId) => (
|
||||||
|
requestJson(`/plants/${companyId}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updatePlantAlerts = (companyId, plantId, alertsEnabled) => {
|
||||||
|
if (!companyId) {
|
||||||
|
throw new Error('발전소의 업체 ID가 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestJson(`/plants/${companyId}/${plantId}/alerts`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ alerts_enabled: alertsEnabled }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHourlyPlantStats = (plantId, date) => (
|
||||||
|
requestJson(`/plants/${plantId}/stats/today${buildQuery({ date })}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getPlantStats = (plantId, params) => (
|
||||||
|
requestJson(`/plants/${plantId}/stats${buildQuery(params)}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getComparisonStats = (params) => (
|
||||||
|
requestJson('/plants/stats/comparison' + buildQuery(params))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const uploadGenerationData = (plantId, uploadType, formData) => {
|
||||||
|
const suffix = uploadType === 'monthly' ? '/monthly' : '';
|
||||||
|
return requestJson(`/plants/${plantId}/upload${suffix}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
};
|
||||||
87
app/utils/statsChart.js
Normal file
87
app/utils/statsChart.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
const EMPTY_BAR_COLOR = '#E5E7EB';
|
||||||
|
const DATA_BAR_COLOR = '#3B82F6';
|
||||||
|
const CURRENT_BAR_COLOR = '#10B981';
|
||||||
|
|
||||||
|
export const toLocalIsoDate = (value) => {
|
||||||
|
const year = value.getFullYear();
|
||||||
|
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(value.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelTextStyle = (shiftX) => ({
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#6B7280',
|
||||||
|
width: 40,
|
||||||
|
textAlign: 'center',
|
||||||
|
shiftX,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatYearLabel = (label) => (
|
||||||
|
label?.length === 4 ? `${label.slice(2)}년` : (label || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formatHourlyChartData = (hourlyData, selectedDate, now = new Date()) => {
|
||||||
|
const byHour = new Map(hourlyData.map((item) => [item.hour, item]));
|
||||||
|
const isToday = toLocalIsoDate(now) === toLocalIsoDate(selectedDate);
|
||||||
|
|
||||||
|
return Array.from({ length: 25 }, (_, hour) => {
|
||||||
|
if (hour === 24) {
|
||||||
|
return { value: 0, label: '24시', frontColor: 'transparent' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = byHour.get(hour) || {};
|
||||||
|
const hasData = item.has_data || item.current_kw > 0;
|
||||||
|
const isCurrentHour = isToday && hour === now.getHours();
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: item.current_kw || 0,
|
||||||
|
label: hour % 2 === 0 ? `${hour}시` : '',
|
||||||
|
frontColor: isCurrentHour
|
||||||
|
? CURRENT_BAR_COLOR
|
||||||
|
: (hasData ? DATA_BAR_COLOR : EMPTY_BAR_COLOR),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatPeriodChartData = (period, rawData, selectedDate) => {
|
||||||
|
const year = selectedDate.getFullYear();
|
||||||
|
const month = selectedDate.getMonth() + 1;
|
||||||
|
const dataMap = new Map(rawData.map((item) => [item.label, item.value || 0]));
|
||||||
|
|
||||||
|
if (period === 'day') {
|
||||||
|
const lastDay = new Date(year, month, 0).getDate();
|
||||||
|
return Array.from({ length: lastDay }, (_, index) => {
|
||||||
|
const day = index + 1;
|
||||||
|
const key = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
const value = dataMap.get(key) || 0;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: day === 1 || day % 5 === 0 ? `${day}` : '',
|
||||||
|
labelTextStyle: labelTextStyle(-10),
|
||||||
|
frontColor: value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === 'month') {
|
||||||
|
return Array.from({ length: 12 }, (_, index) => {
|
||||||
|
const itemMonth = index + 1;
|
||||||
|
const key = `${year}-${String(itemMonth).padStart(2, '0')}`;
|
||||||
|
const value = dataMap.get(key) || 0;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: `${itemMonth}월`,
|
||||||
|
labelTextStyle: labelTextStyle(-5),
|
||||||
|
frontColor: value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawData.map((item) => ({
|
||||||
|
value: item.value || 0,
|
||||||
|
label: formatYearLabel(item.label),
|
||||||
|
labelTextStyle: labelTextStyle(-10),
|
||||||
|
frontColor: item.value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
@ -6,14 +6,12 @@
|
||||||
NAS 배포 전 Windows 환경에서 테스트 및 데이터 복구를 수행할 수 있습니다.
|
NAS 배포 전 Windows 환경에서 테스트 및 데이터 복구를 수행할 수 있습니다.
|
||||||
|
|
||||||
## 2. 가상환경 (Windows)
|
## 2. 가상환경 (Windows)
|
||||||
윈도우용 가상환경은 `venv_win` 폴더에 구성되어 있습니다.
|
가상환경은 저장소에 포함하지 않고 개발 PC마다 `.venv`로 생성합니다. 고정 의존성은 `requirements.txt`를 사용합니다.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 가상환경 활성화
|
python -m venv .venv
|
||||||
.\venv_win\Scripts\activate
|
.\.venv\Scripts\python -m pip install -r requirements.txt
|
||||||
|
.\.venv\Scripts\python -m unittest discover -s tests -p "test_*.py" -v
|
||||||
# 의존성 설치
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 개발 도구 (tools 폴더)
|
## 3. 개발 도구 (tools 폴더)
|
||||||
|
|
@ -35,16 +33,16 @@ pip install -r requirements.txt
|
||||||
python tools/recover_from_log.py
|
python tools/recover_from_log.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 실행 및 테스트
|
## 4. 실행
|
||||||
메인 크롤러 실행:
|
메인 크롤러 실행:
|
||||||
```powershell
|
```powershell
|
||||||
python main.py
|
.\.venv\Scripts\python main.py
|
||||||
```
|
```
|
||||||
GUI 대시보드 실행 (테스트용):
|
GUI 대시보드 실행 (테스트용):
|
||||||
```powershell
|
```powershell
|
||||||
python crawler_gui.py
|
.\.venv\Scripts\python crawler_gui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. 배포 시 주의사항
|
## 5. 배포 시 주의사항
|
||||||
- `crawler_manager.py`의 `site_data.db`는 로컬에 생성되므로 배포 시 제외하거나 초기화 상태로 배포하세요.
|
- `crawler_manager.py`의 `crawler_manager.db`는 런타임에 생성되며 Git에서 제외합니다. 운영 상태 DB를 배포 파일로 덮어쓰지 마세요.
|
||||||
- `.env` 파일의 API 키가 만료되지 않았는지 확인하세요.
|
- `.env` 파일의 API 키가 만료되지 않았는지 확인하세요.
|
||||||
|
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
# ==========================================
|
|
||||||
# crawlers/cmsolar.py - CMSolar 크롤러 (10호기)
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from .base import create_session
|
|
||||||
|
|
||||||
def fetch_data(plant_info):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소 데이터 수집
|
|
||||||
"""
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
company_name = plant_info.get('company_name', '태양과바람')
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('login_id', '')
|
|
||||||
login_pw = auth.get('login_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0 Safari/537.36',
|
|
||||||
'Referer': f'{base_url}/plant/index.php'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
try:
|
|
||||||
login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'}
|
|
||||||
session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers)
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 2. 사이트 선택
|
|
||||||
try:
|
|
||||||
session.get(f"{base_url}/change.php?site={site_no}", headers=headers)
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 3. 데이터 요청
|
|
||||||
target_url = f"{base_url}/plant/sub/idx_ok.php?mode=getPlant"
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(target_url, headers=headers)
|
|
||||||
res.encoding = 'utf-8'
|
|
||||||
|
|
||||||
data = res.json()
|
|
||||||
plant_data = data[0]['plant']
|
|
||||||
|
|
||||||
# 단위 변환 (W -> kW, Wh -> kWh)
|
|
||||||
curr_kw = float(plant_data.get('now', 0)) / 1000
|
|
||||||
today_kwh = float(plant_data.get('today', 0)) / 1000
|
|
||||||
|
|
||||||
is_error = int(plant_data.get('inv_error', 0))
|
|
||||||
status = "🟢 정상" if is_error == 0 else "🔴 점검/고장"
|
|
||||||
|
|
||||||
print(f" [CMSolar] {plant_name} 수집 완료: {round(curr_kw, 2)} kW")
|
|
||||||
return [{
|
|
||||||
'id': plant_id,
|
|
||||||
'name': f'{company_name} {plant_name}',
|
|
||||||
'kw': round(curr_kw, 2),
|
|
||||||
'today': round(today_kwh, 2),
|
|
||||||
'status': status
|
|
||||||
}]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_daily(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 일별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('login_id', '')
|
|
||||||
login_pw = auth.get('login_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar Daily] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Referer': f'{base_url}/plant/index.php'
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'}
|
|
||||||
session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers)
|
|
||||||
session.get(f"{base_url}/change.php?site={site_no}", headers=headers)
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login failed: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# 일별 데이터 엔드포인트 (추정)
|
|
||||||
daily_url = f"{base_url}/plant/sub/daily_data.php?date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(daily_url, headers=headers, timeout=10)
|
|
||||||
res.encoding = 'utf-8'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
daily_kwh = safe_float(data.get('today', data.get('daily', 0))) / 1000.0
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'date': date_str,
|
|
||||||
'generation_kwh': daily_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {date_str}: {daily_kwh}kWh")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ {date_str}: {e}")
|
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
|
||||||
|
|
||||||
print(f"[Total] Collected {len(results)} daily records\n")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_monthly(plant_info, start_month, end_month):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 월별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from .base import safe_float
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('login_id', '')
|
|
||||||
login_pw = auth.get('login_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar Monthly] {plant_name} ({start_month} ~ {end_month})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Referer': f'{base_url}/plant/index.php'
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'}
|
|
||||||
session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers)
|
|
||||||
session.get(f"{base_url}/change.php?site={site_no}", headers=headers)
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login failed: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# 월별 데이터 엔드포인트 (추정)
|
|
||||||
monthly_url = f"{base_url}/plant/sub/monthly_data.php?month={month_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(monthly_url, headers=headers, timeout=10)
|
|
||||||
res.encoding = 'utf-8'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
monthly_kwh = safe_float(data.get('month', data.get('monthly', 0))) / 1000.0
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'month': month_str,
|
|
||||||
'generation_kwh': monthly_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {month_str}: {monthly_kwh}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
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_hourly(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 시간대별 과거 데이터 수집
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plant_info: dict, 발전소 정보
|
|
||||||
start_date: str, 시작일 (YYYY-MM-DD)
|
|
||||||
end_date: str, 종료일 (YYYY-MM-DD)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 시간대별 데이터 레코드
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# 설정 추출
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('login_id', '')
|
|
||||||
login_pw = auth.get('login_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar History] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
# 로그인
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0 Safari/537.36',
|
|
||||||
'Referer': f'{base_url}/plant/index.php'
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'}
|
|
||||||
session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers)
|
|
||||||
|
|
||||||
# 사이트 선택
|
|
||||||
session.get(f"{base_url}/change.php?site={site_no}", headers=headers)
|
|
||||||
print(f" ✓ Login successful")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login failed: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 날짜 범위 반복
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
print(f"\n[Processing Date] {date_str}")
|
|
||||||
|
|
||||||
# 시간대별 데이터 엔드포인트 (추정)
|
|
||||||
hourly_url = f"{base_url}/plant/sub/hourly_data.php?site={site_no}&date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(hourly_url, headers=headers, timeout=10)
|
|
||||||
res.encoding = 'utf-8'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
hourly_data = data if isinstance(data, list) else data.get('hourly', [])
|
|
||||||
|
|
||||||
if hourly_data and len(hourly_data) > 0:
|
|
||||||
print(f" ✓ Found {len(hourly_data)} hourly records")
|
|
||||||
|
|
||||||
for item in hourly_data:
|
|
||||||
hour = str(item.get('hour', item.get('time', '00'))).zfill(2)
|
|
||||||
generation_wh = safe_float(item.get('energy', item.get('now', 0)))
|
|
||||||
generation_kwh = generation_wh / 1000.0 if generation_wh > 1000 else generation_wh
|
|
||||||
current_kw = safe_float(item.get('power', 0)) / 1000.0
|
|
||||||
|
|
||||||
timestamp = f"{date_str} {hour}:00:00"
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'generation_kwh': generation_kwh,
|
|
||||||
'current_kw': current_kw
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
print(f" ⚠ No hourly 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)} hourly records")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
@ -1,427 +0,0 @@
|
||||||
# ==========================================
|
|
||||||
# crawlers/cmsolar.py - CMSolar 크롤러 (10호기)
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from .base import create_session, safe_float
|
|
||||||
|
|
||||||
def fetch_data(plant_info):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소 데이터 수집
|
|
||||||
"""
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
company_name = plant_info.get('company_name', '함안햇빛발전소')
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('payload_id', '')
|
|
||||||
login_pw = auth.get('payload_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
data_url = system.get('data_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 로그인
|
|
||||||
login_data = {
|
|
||||||
'login_id': login_id,
|
|
||||||
'login_pw': login_pw,
|
|
||||||
'site_no': site_no
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code != 200:
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 접속 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 데이터 요청
|
|
||||||
try:
|
|
||||||
res = session.get(data_url, headers=headers)
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
curr_kw = safe_float(data.get('current', data.get('power', 0)))
|
|
||||||
today_kwh = safe_float(data.get('today', data.get('generation', 0)))
|
|
||||||
status = "🟢 정상" if curr_kw > 0 else "💤 대기"
|
|
||||||
|
|
||||||
return [{
|
|
||||||
'id': plant_id,
|
|
||||||
'name': f'{company_name} {plant_name}',
|
|
||||||
'kw': curr_kw,
|
|
||||||
'today': today_kwh,
|
|
||||||
'status': status
|
|
||||||
}]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_hourly(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 시간대별 과거 데이터 수집
|
|
||||||
|
|
||||||
실제 엔드포인트: /plant/sub/report_ok.php
|
|
||||||
파라미터: mode=getPowers&type=daily&device=total&start=YYYY-MM-DD&money=
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('payload_id', '')
|
|
||||||
login_pw = auth.get('payload_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
|
|
||||||
# 실제 데이터 엔드포인트
|
|
||||||
base_url = system.get('api_url', 'http://www.cmsolar2.kr')
|
|
||||||
data_url = f"{base_url}/plant/sub/report_ok.php"
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar History] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
'login_id': login_id,
|
|
||||||
'login_pw': login_pw,
|
|
||||||
'site_no': site_no
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 사이트 선택 (필수!)
|
|
||||||
try:
|
|
||||||
change_url = f"{base_url}/change.php?site={site_no}"
|
|
||||||
session.get(change_url, headers=headers)
|
|
||||||
print(" ✓ Site selected")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Site selection error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 날짜 반복
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# 실제 확인된 시간별 엔드포인트 (type=daily는 하루 치 시간별 데이터 반환)
|
|
||||||
params = {
|
|
||||||
'mode': 'getPowers',
|
|
||||||
'type': 'daily',
|
|
||||||
'device': 'total',
|
|
||||||
'start': date_str,
|
|
||||||
'money': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(data_url, params=params, headers=headers, timeout=10)
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
# 시간별 데이터 파싱
|
|
||||||
hourly_data = data.get('data', []) or data.get('list', []) or data.get('powers', [])
|
|
||||||
|
|
||||||
if isinstance(hourly_data, list) and len(hourly_data) > 0:
|
|
||||||
print(f" ✓ Found {len(hourly_data)} hourly records for {date_str}")
|
|
||||||
|
|
||||||
for item in hourly_data:
|
|
||||||
hour = str(item.get('hour', item.get('time', '00'))).zfill(2)
|
|
||||||
generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0))))
|
|
||||||
current_kw = safe_float(item.get('kw', 0))
|
|
||||||
|
|
||||||
timestamp = f"{date_str} {hour}:00:00"
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'generation_kwh': generation_kwh,
|
|
||||||
'current_kw': current_kw
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
print(f" ⚠ No data for {date_str}")
|
|
||||||
else:
|
|
||||||
print(f" ✗ HTTP {res.status_code} for {date_str}")
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 일별 과거 데이터 수집
|
|
||||||
|
|
||||||
실제 엔드포인트: /plant/sub/report_ok.php
|
|
||||||
파라미터: mode=getPowers&type=month&device=total&start=YYYY-MM-DD&money=
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('payload_id', '')
|
|
||||||
login_pw = auth.get('payload_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
|
|
||||||
# 실제 데이터 엔드포인트
|
|
||||||
base_url = system.get('api_url', 'http://www.cmsolar2.kr')
|
|
||||||
data_url = f"{base_url}/plant/sub/report_ok.php"
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar Daily] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
'login_id': login_id,
|
|
||||||
'login_pw': login_pw,
|
|
||||||
'site_no': site_no
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 사이트 선택 (필수!)
|
|
||||||
try:
|
|
||||||
change_url = f"{base_url}/change.php?site={site_no}"
|
|
||||||
session.get(change_url, headers=headers)
|
|
||||||
print(" ✓ Site selected")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Site selection error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 월 단위로 반복 (type=month는 한 달 치 일별 데이터 반환)
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
month_start = current_date.strftime('%Y-%m-01')
|
|
||||||
|
|
||||||
# 실제 확인된 일별 엔드포인트 (type=month)
|
|
||||||
params = {
|
|
||||||
'mode': 'getPowers',
|
|
||||||
'type': 'month',
|
|
||||||
'device': 'total',
|
|
||||||
'start': month_start,
|
|
||||||
'money': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(data_url, params=params, headers=headers, timeout=10)
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
# 일별 데이터 파싱
|
|
||||||
daily_data = data.get('data', []) or data.get('list', []) or data.get('powers', [])
|
|
||||||
|
|
||||||
if isinstance(daily_data, list) and len(daily_data) > 0:
|
|
||||||
print(f" ✓ Found {len(daily_data)} daily records for {month_start[:7]}")
|
|
||||||
|
|
||||||
for item in daily_data:
|
|
||||||
date_str = item.get('date', item.get('day', ''))
|
|
||||||
generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0))))
|
|
||||||
current_kw = safe_float(item.get('kw', 0))
|
|
||||||
|
|
||||||
# 날짜 범위 필터링
|
|
||||||
if date_str >= start_date and date_str <= end_date:
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'date': date_str,
|
|
||||||
'generation_kwh': generation_kwh,
|
|
||||||
'current_kw': current_kw
|
|
||||||
})
|
|
||||||
print(f" ✓ {date_str}: {generation_kwh:.2f}kWh")
|
|
||||||
else:
|
|
||||||
print(f" ✗ HTTP {res.status_code} for {month_start[:7]}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Error: {e}")
|
|
||||||
|
|
||||||
# 다음 달로 이동
|
|
||||||
current_date = (current_date.replace(day=1) + relativedelta(months=1))
|
|
||||||
|
|
||||||
print(f"[Total] Collected {len(results)} daily records\n")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_monthly(plant_info, start_month, end_month):
|
|
||||||
"""
|
|
||||||
CMSolar 발전소의 월별 과거 데이터 수집
|
|
||||||
|
|
||||||
실제 엔드포인트: /plant/sub/report_ok.php
|
|
||||||
파라미터: mode=getPowers&type=year&device=total&start=YYYY-MM-DD&money=
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'cmsolar-10')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '10호기')
|
|
||||||
|
|
||||||
login_id = auth.get('payload_id', '')
|
|
||||||
login_pw = auth.get('payload_pw', '')
|
|
||||||
site_no = auth.get('site_no', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
|
|
||||||
# 실제 데이터 엔드포인트
|
|
||||||
base_url = system.get('api_url', 'http://www.cmsolar2.kr')
|
|
||||||
data_url = f"{base_url}/plant/sub/report_ok.php"
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[CMSolar Monthly] {plant_name} ({start_month} ~ {end_month})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
'login_id': login_id,
|
|
||||||
'login_pw': login_pw,
|
|
||||||
'site_no': site_no
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
# 사이트 선택 (필수!)
|
|
||||||
try:
|
|
||||||
change_url = f"{base_url}/change.php?site={site_no}"
|
|
||||||
session.get(change_url, headers=headers)
|
|
||||||
print(" ✓ Site selected")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Site selection error: {e}")
|
|
||||||
return results
|
|
||||||
# 연도별로 반복 (type=year는 한 해 치 월별 데이터 반환)
|
|
||||||
current_date = datetime.strptime(start_month + '-01', '%Y-%m-%d')
|
|
||||||
end_date = datetime.strptime(end_month + '-01', '%Y-%m-%d')
|
|
||||||
|
|
||||||
years_processed = set()
|
|
||||||
|
|
||||||
while current_date <= end_date:
|
|
||||||
year_start = current_date.strftime('%Y-01-01')
|
|
||||||
year = current_date.year
|
|
||||||
|
|
||||||
# 중복 연도 스킵
|
|
||||||
if year in years_processed:
|
|
||||||
current_date += relativedelta(months=1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
years_processed.add(year)
|
|
||||||
|
|
||||||
# 실제 확인된 월별 엔드포인트 (type=year)
|
|
||||||
params = {
|
|
||||||
'mode': 'getPowers',
|
|
||||||
'type': 'year',
|
|
||||||
'device': 'total',
|
|
||||||
'start': year_start,
|
|
||||||
'money': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.get(data_url, params=params, headers=headers, timeout=10)
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
# 월별 데이터 파싱
|
|
||||||
monthly_data = data.get('data', []) or data.get('list', []) or data.get('powers', [])
|
|
||||||
|
|
||||||
if isinstance(monthly_data, list) and len(monthly_data) > 0:
|
|
||||||
print(f" ✓ Found {len(monthly_data)} monthly records for {year}")
|
|
||||||
|
|
||||||
for item in monthly_data:
|
|
||||||
month_str = item.get('month', item.get('date', ''))
|
|
||||||
generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0))))
|
|
||||||
|
|
||||||
# YYYY-MM 형식으로 정규화
|
|
||||||
if len(month_str) >= 7:
|
|
||||||
month_str = month_str[:7]
|
|
||||||
|
|
||||||
# 월 범위 필터링
|
|
||||||
if month_str >= start_month and month_str <= end_month:
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'month': month_str,
|
|
||||||
'generation_kwh': generation_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {month_str}: {generation_kwh:.1f}kWh")
|
|
||||||
else:
|
|
||||||
print(f" ✗ HTTP {res.status_code} for {year}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Error: {e}")
|
|
||||||
|
|
||||||
current_date += relativedelta(months=1)
|
|
||||||
|
|
||||||
print(f"[Total] Collected {len(results)} monthly records\n")
|
|
||||||
return results
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
# ==========================================
|
|
||||||
# crawlers/sun_wms.py - Sun-WMS 크롤러 (6호기)
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from .base import create_session
|
|
||||||
|
|
||||||
def fetch_data(plant_info):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소 데이터 수집
|
|
||||||
"""
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
company_name = plant_info.get('company_name', '태양과바람')
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
data_url = system.get('data_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
|
|
||||||
'Referer': 'http://tb6.sun-wms.com/public/main/login.php',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
login_data = {
|
|
||||||
'act': 'loginChk',
|
|
||||||
'user_id': payload_id,
|
|
||||||
'user_pass': payload_pw
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code != 200:
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 접속 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 2. 데이터 요청
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{data_url}?time={timestamp}", headers=headers)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
content = res.text
|
|
||||||
|
|
||||||
match_kw = re.search(r"id=['\"]cur_power['\"].*?value=['\"]([^'\"]+)['\"]", content)
|
|
||||||
curr_kw = float(match_kw.group(1)) if match_kw else 0.0
|
|
||||||
|
|
||||||
match_today = re.search(r"id=['\"]today_power['\"].*?value=['\"]([^'\"]+)['\"]", content)
|
|
||||||
today_kwh = float(match_today.group(1)) if match_today else 0.0
|
|
||||||
|
|
||||||
status = "🟢 정상" if curr_kw > 0 else "💤 대기"
|
|
||||||
|
|
||||||
return [{
|
|
||||||
'id': plant_id,
|
|
||||||
'name': f'{company_name} {plant_name}',
|
|
||||||
'kw': curr_kw,
|
|
||||||
'today': today_kwh,
|
|
||||||
'status': status
|
|
||||||
}]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_daily(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 일별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS Daily] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# 일별 데이터 엔드포인트 (추정)
|
|
||||||
daily_url = f"{base_url}/public/chart/getDailyData.php?date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{daily_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
daily_kwh = safe_float(data.get('daily', data.get('today', 0)))
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'date': date_str,
|
|
||||||
'generation_kwh': daily_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {date_str}: {daily_kwh}kWh")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ {date_str}: {e}")
|
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
|
||||||
|
|
||||||
print(f"[Total] Collected {len(results)} daily records\n")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_monthly(plant_info, start_month, end_month):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 월별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS Monthly] {plant_name} ({start_month} ~ {end_month})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# 월별 데이터 엔드포인트 (추정)
|
|
||||||
monthly_url = f"{base_url}/public/chart/getMonthlyData.php?month={month_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{monthly_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
monthly_kwh = safe_float(data.get('monthly', data.get('month', 0)))
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'month': month_str,
|
|
||||||
'generation_kwh': monthly_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {month_str}: {monthly_kwh}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
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_hourly(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 시간대별 과거 데이터 수집
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plant_info: dict, 발전소 정보
|
|
||||||
start_date: str, 시작일 (YYYY-MM-DD)
|
|
||||||
end_date: str, 종료일 (YYYY-MM-DD)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 시간대별 데이터 레코드
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# 설정 추출
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS 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',
|
|
||||||
'Referer': 'http://tb6.sun-wms.com/public/main/login.php',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
'act': 'loginChk',
|
|
||||||
'user_id': payload_id,
|
|
||||||
'user_pass': payload_pw
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code != 200:
|
|
||||||
print(f" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
|
|
||||||
print(f" ✓ Login successful")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 날짜 범위 반복
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
print(f"\n[Processing Date] {date_str}")
|
|
||||||
|
|
||||||
# 시간대별 데이터 엔드포인트 (추정)
|
|
||||||
hourly_url = f"{base_url}/public/chart/getHourlyData.php?date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{hourly_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
hourly_data = data if isinstance(data, list) else data.get('hourly', [])
|
|
||||||
|
|
||||||
if hourly_data and len(hourly_data) > 0:
|
|
||||||
print(f" ✓ Found {len(hourly_data)} hourly records")
|
|
||||||
|
|
||||||
for item in hourly_data:
|
|
||||||
hour = str(item.get('hour', item.get('time', '00'))).zfill(2)
|
|
||||||
generation_kwh = safe_float(item.get('power', item.get('kwh', 0)))
|
|
||||||
current_kw = safe_float(item.get('kw', 0))
|
|
||||||
|
|
||||||
timestamp = f"{date_str} {hour}:00:00"
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'generation_kwh': generation_kwh,
|
|
||||||
'current_kw': current_kw
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
print(f" ⚠ No hourly 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)} hourly records")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
# ==========================================
|
|
||||||
# crawlers/sun_wms.py - Sun-WMS 크롤러 (6호기)
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from .base import create_session
|
|
||||||
|
|
||||||
def fetch_data(plant_info):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소 데이터 수집
|
|
||||||
"""
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
company_name = plant_info.get('company_name', '태양과바람')
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
data_url = system.get('data_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
|
|
||||||
'Referer': 'http://tb6.sun-wms.com/public/main/login.php',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
login_data = {
|
|
||||||
'act': 'loginChk',
|
|
||||||
'user_id': payload_id,
|
|
||||||
'user_pass': payload_pw
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code != 200:
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 접속 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 2. 데이터 요청
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{data_url}?time={timestamp}", headers=headers)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
content = res.text
|
|
||||||
|
|
||||||
match_kw = re.search(r"id=['\"]cur_power['\"].*?value=['\"]([^'\"]+)['\"]", content)
|
|
||||||
curr_kw = float(match_kw.group(1)) if match_kw else 0.0
|
|
||||||
|
|
||||||
match_today = re.search(r"id=['\"]today_power['\"].*?value=['\"]([^'\"]+)['\"]", content)
|
|
||||||
today_kwh = float(match_today.group(1)) if match_today else 0.0
|
|
||||||
|
|
||||||
status = "🟢 정상" if curr_kw > 0 else "💤 대기"
|
|
||||||
|
|
||||||
return [{
|
|
||||||
'id': plant_id,
|
|
||||||
'name': f'{company_name} {plant_name}',
|
|
||||||
'kw': curr_kw,
|
|
||||||
'today': today_kwh,
|
|
||||||
'status': status
|
|
||||||
}]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {plant_name} 에러: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_daily(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 일별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS Daily] {plant_name} ({start_date} ~ {end_date})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# 일별 데이터 엔드포인트 (추정)
|
|
||||||
daily_url = f"{base_url}/public/chart/getDailyData.php?date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{daily_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
daily_kwh = safe_float(data.get('daily', data.get('today', 0)))
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'date': date_str,
|
|
||||||
'generation_kwh': daily_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {date_str}: {daily_kwh}kWh")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ {date_str}: {e}")
|
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
|
||||||
|
|
||||||
print(f"[Total] Collected {len(results)} daily records\n")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_monthly(plant_info, start_month, end_month):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 월별 과거 데이터 수집
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS Monthly] {plant_name} ({start_month} ~ {end_month})")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code == 200:
|
|
||||||
print(" ✓ Login successful")
|
|
||||||
else:
|
|
||||||
print(" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# 월별 데이터 엔드포인트 (추정)
|
|
||||||
monthly_url = f"{base_url}/public/chart/getMonthlyData.php?month={month_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{monthly_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
monthly_kwh = safe_float(data.get('monthly', data.get('month', 0)))
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'month': month_str,
|
|
||||||
'generation_kwh': monthly_kwh
|
|
||||||
})
|
|
||||||
print(f" ✓ {month_str}: {monthly_kwh}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
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_history_hourly(plant_info, start_date, end_date):
|
|
||||||
"""
|
|
||||||
Sun-WMS 발전소의 시간대별 과거 데이터 수집
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plant_info: dict, 발전소 정보
|
|
||||||
start_date: str, 시작일 (YYYY-MM-DD)
|
|
||||||
end_date: str, 종료일 (YYYY-MM-DD)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 시간대별 데이터 레코드
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from .base import safe_float
|
|
||||||
import time
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# 설정 추출
|
|
||||||
plant_id = plant_info.get('id', 'sunwms-06')
|
|
||||||
auth = plant_info.get('auth', {})
|
|
||||||
system = plant_info.get('system', {})
|
|
||||||
plant_name = plant_info.get('name', '6호기')
|
|
||||||
|
|
||||||
payload_id = auth.get('payload_id', '')
|
|
||||||
payload_pw = auth.get('payload_pw', '')
|
|
||||||
login_url = system.get('login_url', '')
|
|
||||||
base_url = system.get('base_url', '')
|
|
||||||
|
|
||||||
session = create_session()
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[Sun-WMS 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',
|
|
||||||
'Referer': 'http://tb6.sun-wms.com/public/main/login.php',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
'act': 'loginChk',
|
|
||||||
'user_id': payload_id,
|
|
||||||
'user_pass': payload_pw
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = session.post(login_url, data=login_data, headers=headers)
|
|
||||||
if res.status_code != 200:
|
|
||||||
print(f" ✗ Login failed")
|
|
||||||
return results
|
|
||||||
|
|
||||||
print(f" ✓ Login successful")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login error: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
# 날짜 범위 반복
|
|
||||||
current_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
||||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
||||||
|
|
||||||
while current_date <= end_dt:
|
|
||||||
date_str = current_date.strftime('%Y-%m-%d')
|
|
||||||
print(f"\n[Processing Date] {date_str}")
|
|
||||||
|
|
||||||
# 시간대별 데이터 엔드포인트 (추정)
|
|
||||||
hourly_url = f"{base_url}/public/chart/getHourlyData.php?date={date_str}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
res = session.get(f"{hourly_url}&time={timestamp}", headers=headers, timeout=10)
|
|
||||||
res.encoding = 'euc-kr'
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
hourly_data = data if isinstance(data, list) else data.get('hourly', [])
|
|
||||||
|
|
||||||
if hourly_data and len(hourly_data) > 0:
|
|
||||||
print(f" ✓ Found {len(hourly_data)} hourly records")
|
|
||||||
|
|
||||||
for item in hourly_data:
|
|
||||||
hour = str(item.get('hour', item.get('time', '00'))).zfill(2)
|
|
||||||
generation_kwh = safe_float(item.get('power', item.get('kwh', 0)))
|
|
||||||
current_kw = safe_float(item.get('kw', 0))
|
|
||||||
|
|
||||||
timestamp = f"{date_str} {hour}:00:00"
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'plant_id': plant_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'generation_kwh': generation_kwh,
|
|
||||||
'current_kw': current_kw
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
print(f" ⚠ No hourly 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)} hourly records")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
@ -112,11 +112,13 @@
|
||||||
|
|
||||||
## 🔧 사용 방법
|
## 🔧 사용 방법
|
||||||
|
|
||||||
모든 스크립트는 crawler 가상환경에서 실행:
|
모든 스크립트는 `docs/development_and_testing.md`에 따라 새로 만든 crawler 가상환경에서 실행:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd d:\dev\etc\SolorPower\crawler
|
cd d:\dev\etc\SolorPower\crawler
|
||||||
.\venv_win\Scripts\Activate.ps1
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
|
||||||
# 스크립트 실행 예시
|
# 스크립트 실행 예시
|
||||||
python scripts_archive/fetch_february.py
|
python scripts_archive/fetch_february.py
|
||||||
|
|
|
||||||
|
|
@ -1,502 +0,0 @@
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Activate a Python virtual environment for the current PowerShell session.
|
|
||||||
|
|
||||||
.Description
|
|
||||||
Pushes the python executable for a virtual environment to the front of the
|
|
||||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
|
||||||
in a Python virtual environment. Makes use of the command line switches as
|
|
||||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
|
||||||
|
|
||||||
.Parameter VenvDir
|
|
||||||
Path to the directory that contains the virtual environment to activate. The
|
|
||||||
default value for this is the parent of the directory that the Activate.ps1
|
|
||||||
script is located within.
|
|
||||||
|
|
||||||
.Parameter Prompt
|
|
||||||
The prompt prefix to display when this virtual environment is activated. By
|
|
||||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
|
||||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Verbose
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and shows extra information about the activation as it executes.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
|
||||||
Activates the Python virtual environment located in the specified location.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Prompt "MyPython"
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and prefixes the current prompt with the specified string (surrounded in
|
|
||||||
parentheses) while the virtual environment is active.
|
|
||||||
|
|
||||||
.Notes
|
|
||||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
|
||||||
execution policy for the user. You can do this by issuing the following PowerShell
|
|
||||||
command:
|
|
||||||
|
|
||||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
||||||
|
|
||||||
For more information on Execution Policies:
|
|
||||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
|
||||||
|
|
||||||
#>
|
|
||||||
Param(
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$VenvDir,
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$Prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
<# Function declarations --------------------------------------------------- #>
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Remove all shell session elements added by the Activate script, including the
|
|
||||||
addition of the virtual environment's Python executable from the beginning of
|
|
||||||
the PATH variable.
|
|
||||||
|
|
||||||
.Parameter NonDestructive
|
|
||||||
If present, do not remove this function from the global namespace for the
|
|
||||||
session.
|
|
||||||
|
|
||||||
#>
|
|
||||||
function global:deactivate ([switch]$NonDestructive) {
|
|
||||||
# Revert to original values
|
|
||||||
|
|
||||||
# The prior prompt:
|
|
||||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
|
||||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
|
||||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PYTHONHOME:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PATH:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the VIRTUAL_ENV altogether:
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
|
||||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
|
||||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# Leave deactivate function in the global namespace if requested:
|
|
||||||
if (-not $NonDestructive) {
|
|
||||||
Remove-Item -Path function:deactivate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Description
|
|
||||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
|
||||||
given folder, and returns them in a map.
|
|
||||||
|
|
||||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
|
||||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
|
||||||
then it is considered a `key = value` line. The left hand string is the key,
|
|
||||||
the right hand is the value.
|
|
||||||
|
|
||||||
If the value starts with a `'` or a `"` then the first and last character is
|
|
||||||
stripped from the value before being captured.
|
|
||||||
|
|
||||||
.Parameter ConfigDir
|
|
||||||
Path to the directory that contains the `pyvenv.cfg` file.
|
|
||||||
#>
|
|
||||||
function Get-PyVenvConfig(
|
|
||||||
[String]
|
|
||||||
$ConfigDir
|
|
||||||
) {
|
|
||||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
|
||||||
|
|
||||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
|
||||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
|
||||||
|
|
||||||
# An empty map will be returned if no config file is found.
|
|
||||||
$pyvenvConfig = @{ }
|
|
||||||
|
|
||||||
if ($pyvenvConfigPath) {
|
|
||||||
|
|
||||||
Write-Verbose "File exists, parse `key = value` lines"
|
|
||||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
|
||||||
|
|
||||||
$pyvenvConfigContent | ForEach-Object {
|
|
||||||
$keyval = $PSItem -split "\s*=\s*", 2
|
|
||||||
if ($keyval[0] -and $keyval[1]) {
|
|
||||||
$val = $keyval[1]
|
|
||||||
|
|
||||||
# Remove extraneous quotations around a string value.
|
|
||||||
if ("'""".Contains($val.Substring(0, 1))) {
|
|
||||||
$val = $val.Substring(1, $val.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
$pyvenvConfig[$keyval[0]] = $val
|
|
||||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $pyvenvConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<# Begin Activate script --------------------------------------------------- #>
|
|
||||||
|
|
||||||
# Determine the containing directory of this script
|
|
||||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
|
||||||
|
|
||||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
|
||||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
|
||||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
|
||||||
|
|
||||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
|
||||||
# First, get the location of the virtual environment, it might not be
|
|
||||||
# VenvExecDir if specified on the command line.
|
|
||||||
if ($VenvDir) {
|
|
||||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
|
||||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
|
||||||
Write-Verbose "VenvDir=$VenvDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
|
||||||
# as `prompt`.
|
|
||||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
|
||||||
|
|
||||||
# Next, set the prompt from the command line, or the config file, or
|
|
||||||
# just use the name of the virtual environment folder.
|
|
||||||
if ($Prompt) {
|
|
||||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
|
||||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
|
||||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
|
||||||
$Prompt = $pyvenvCfg['prompt'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
|
||||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
|
||||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Verbose "Prompt = '$Prompt'"
|
|
||||||
Write-Verbose "VenvDir='$VenvDir'"
|
|
||||||
|
|
||||||
# Deactivate any currently active virtual environment, but leave the
|
|
||||||
# deactivate function in place.
|
|
||||||
deactivate -nondestructive
|
|
||||||
|
|
||||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
|
||||||
# that there is an activated venv.
|
|
||||||
$env:VIRTUAL_ENV = $VenvDir
|
|
||||||
|
|
||||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
|
||||||
|
|
||||||
Write-Verbose "Setting prompt to '$Prompt'"
|
|
||||||
|
|
||||||
# Set the prompt to include the env name
|
|
||||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
|
||||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
|
||||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
|
||||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
|
||||||
|
|
||||||
function global:prompt {
|
|
||||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
|
||||||
_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear PYTHONHOME
|
|
||||||
if (Test-Path -Path Env:PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
Remove-Item -Path Env:PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add the venv to the PATH
|
|
||||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
|
||||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
|
||||||
|
|
||||||
# SIG # Begin signature block
|
|
||||||
# MIIvJAYJKoZIhvcNAQcCoIIvFTCCLxECAQExDzANBglghkgBZQMEAgEFADB5Bgor
|
|
||||||
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
|
|
||||||
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnL745ElCYk8vk
|
|
||||||
# dBtMuQhLeWJ3ZGfzKW4DHCYzAn+QB6CCE8MwggWQMIIDeKADAgECAhAFmxtXno4h
|
|
||||||
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
|
|
||||||
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
|
|
||||||
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
|
|
||||||
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
|
|
||||||
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
|
|
||||||
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
|
||||||
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
|
|
||||||
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
|
|
||||||
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
|
|
||||||
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
|
|
||||||
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
|
|
||||||
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
|
|
||||||
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
|
|
||||||
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
|
|
||||||
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
|
|
||||||
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
|
|
||||||
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
|
|
||||||
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
|
|
||||||
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
|
|
||||||
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
|
|
||||||
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
|
|
||||||
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
|
|
||||||
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
|
|
||||||
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
|
|
||||||
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
|
|
||||||
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
|
|
||||||
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
|
|
||||||
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
|
|
||||||
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
|
|
||||||
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
|
|
||||||
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
|
|
||||||
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
|
|
||||||
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
|
|
||||||
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
|
|
||||||
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
|
|
||||||
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
|
|
||||||
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
|
|
||||||
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
|
|
||||||
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
|
|
||||||
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
|
|
||||||
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
|
|
||||||
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
|
|
||||||
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
|
|
||||||
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
|
|
||||||
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
|
|
||||||
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
|
|
||||||
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
|
|
||||||
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
|
|
||||||
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
|
|
||||||
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
|
|
||||||
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
|
|
||||||
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
|
|
||||||
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
|
|
||||||
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
|
|
||||||
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
|
|
||||||
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
|
|
||||||
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
|
|
||||||
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
|
|
||||||
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
|
|
||||||
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
|
|
||||||
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
|
|
||||||
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
|
|
||||||
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
|
|
||||||
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
|
|
||||||
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
|
|
||||||
# eE4wggd3MIIFX6ADAgECAhAHHxQbizANJfMU6yMM0NHdMA0GCSqGSIb3DQEBCwUA
|
|
||||||
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
|
|
||||||
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
|
|
||||||
# ODQgMjAyMSBDQTEwHhcNMjIwMTE3MDAwMDAwWhcNMjUwMTE1MjM1OTU5WjB8MQsw
|
|
||||||
# CQYDVQQGEwJVUzEPMA0GA1UECBMGT3JlZ29uMRIwEAYDVQQHEwlCZWF2ZXJ0b24x
|
|
||||||
# IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQDExpQ
|
|
||||||
# eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
|
|
||||||
# ADCCAgoCggIBAKgc0BTT+iKbtK6f2mr9pNMUTcAJxKdsuOiSYgDFfwhjQy89koM7
|
|
||||||
# uP+QV/gwx8MzEt3c9tLJvDccVWQ8H7mVsk/K+X+IufBLCgUi0GGAZUegEAeRlSXx
|
|
||||||
# xhYScr818ma8EvGIZdiSOhqjYc4KnfgfIS4RLtZSrDFG2tN16yS8skFa3IHyvWdb
|
|
||||||
# D9PvZ4iYNAS4pjYDRjT/9uzPZ4Pan+53xZIcDgjiTwOh8VGuppxcia6a7xCyKoOA
|
|
||||||
# GjvCyQsj5223v1/Ig7Dp9mGI+nh1E3IwmyTIIuVHyK6Lqu352diDY+iCMpk9Zanm
|
|
||||||
# SjmB+GMVs+H/gOiofjjtf6oz0ki3rb7sQ8fTnonIL9dyGTJ0ZFYKeb6BLA66d2GA
|
|
||||||
# LwxZhLe5WH4Np9HcyXHACkppsE6ynYjTOd7+jN1PRJahN1oERzTzEiV6nCO1M3U1
|
|
||||||
# HbPTGyq52IMFSBM2/07WTJSbOeXjvYR7aUxK9/ZkJiacl2iZI7IWe7JKhHohqKuc
|
|
||||||
# eQNyOzxTakLcRkzynvIrk33R9YVqtB4L6wtFxhUjvDnQg16xot2KVPdfyPAWd81w
|
|
||||||
# tZADmrUtsZ9qG79x1hBdyOl4vUtVPECuyhCxaw+faVjumapPUnwo8ygflJJ74J+B
|
|
||||||
# Yxf6UuD7m8yzsfXWkdv52DjL74TxzuFTLHPyARWCSCAbzn3ZIly+qIqDAgMBAAGj
|
|
||||||
# ggIGMIICAjAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfROQjAdBgNVHQ4E
|
|
||||||
# FgQUt/1Teh2XDuUj2WW3siYWJgkZHA8wDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM
|
|
||||||
# MAoGCCsGAQUFBwMDMIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRp
|
|
||||||
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
|
|
||||||
# QTM4NDIwMjFDQTEuY3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20v
|
|
||||||
# RGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0Ex
|
|
||||||
# LmNybDA+BgNVHSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8v
|
|
||||||
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUF
|
|
||||||
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6
|
|
||||||
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu
|
|
||||||
# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZI
|
|
||||||
# hvcNAQELBQADggIBABxv4AeV/5ltkELHSC63fXAFYS5tadcWTiNc2rskrNLrfH1N
|
|
||||||
# s0vgSZFoQxYBFKI159E8oQQ1SKbTEubZ/B9kmHPhprHya08+VVzxC88pOEvz68nA
|
|
||||||
# 82oEM09584aILqYmj8Pj7h/kmZNzuEL7WiwFa/U1hX+XiWfLIJQsAHBla0i7QRF2
|
|
||||||
# de8/VSF0XXFa2kBQ6aiTsiLyKPNbaNtbcucaUdn6vVUS5izWOXM95BSkFSKdE45O
|
|
||||||
# q3FForNJXjBvSCpwcP36WklaHL+aHu1upIhCTUkzTHMh8b86WmjRUqbrnvdyR2yd
|
|
||||||
# I5l1OqcMBjkpPpIV6wcc+KY/RH2xvVuuoHjlUjwq2bHiNoX+W1scCpnA8YTs2d50
|
|
||||||
# jDHUgwUo+ciwpffH0Riq132NFmrH3r67VaN3TuBxjI8SIZM58WEDkbeoriDk3hxU
|
|
||||||
# 8ZWV7b8AW6oyVBGfM06UgkfMb58h+tJPrFx8VI/WLq1dTqMfZOm5cuclMnUHs2uq
|
|
||||||
# rRNtnV8UfidPBL4ZHkTcClQbCoz0UbLhkiDvIS00Dn+BBcxw/TKqVL4Oaz3bkMSs
|
|
||||||
# M46LciTeucHY9ExRVt3zy7i149sd+F4QozPqn7FrSVHXmem3r7bjyHTxOgqxRCVa
|
|
||||||
# 18Vtx7P/8bYSBeS+WHCKcliFCecspusCDSlnRUjZwyPdP0VHxaZg2unjHY3rMYIa
|
|
||||||
# tzCCGrMCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu
|
|
||||||
# Yy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJT
|
|
||||||
# QTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhAHHxQbizANJfMU6yMM0NHdMA0GCWCGSAFl
|
|
||||||
# AwQCAQUAoIHIMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC
|
|
||||||
# AQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBnAZ6P7YvTwq0fbF62
|
|
||||||
# o7E75R0LxsW5OtyYiFESQckLhjBcBgorBgEEAYI3AgEMMU4wTKBGgEQAQgB1AGkA
|
|
||||||
# bAB0ADoAIABSAGUAbABlAGEAcwBlAF8AdgAzAC4AMQAxAC4ANQBfADIAMAAyADMA
|
|
||||||
# MAA4ADIANAAuADAAMaECgAAwDQYJKoZIhvcNAQEBBQAEggIAhs4bX7EeJZ6oSTC7
|
|
||||||
# 5QH/9Qx1Cllidnzj94PqDIL0MiS5adMxYTBkEtP3XNQvYkCtBFc6+Rz7bdN+zWWo
|
|
||||||
# ZYr+sDmEQmRerr3RYyqt+EpgOXpN4BGsHyD7r1Dat1wblGSva8zlOHiIAfzRj2JB
|
|
||||||
# 0+fRJPSBRj9RYwZb5h+I2AFLmHf3yItUdgs8GV9NZsAs+p79dRmoqhgNC6qm8I0j
|
|
||||||
# PkwGr5ATZLyCk2U1+VGeK8iwAdTB4HAlVVM146D/34j/QPnoqe9ICE6Foo6IArVV
|
|
||||||
# CbqWRShWHffvpKaR7ACoTy9LoIQf93orWoc+amQsyaUmlV/zQaCnyjc2UoFCDHcH
|
|
||||||
# 87Yg+frSB8xe2azuKUTVlUDx9Y5wOtEgK+o8wg4ufwPZP0JnsVzN06aCNBz2Bnfb
|
|
||||||
# Mb96Mp0PoCnjp8eAKttmRTXWE0DYIv/XAr2xwwJLFEUdoG6bj0bpNF7Wz0/c3mi0
|
|
||||||
# NKZsd9xNLKBKjizQgCZ7SGCMuSjEnd6P0AI7M8jRx+NROKcJI6gjH0oKXm9JLvI8
|
|
||||||
# oKB2COIlxKEUI/R/kBOeKp53zUSsPFRiJrDEkiCFocAFdUTE326b9/acGbQPJJJ+
|
|
||||||
# nMXLrbTrMMohlj7qRshvO0ZVvpqBDoHlRQcJcfINEESgKNKx/bTpr5cuM3WIS5Ft
|
|
||||||
# 1GirQp9sABVeBom9Y0NDFXsCSkqhghdAMIIXPAYKKwYBBAGCNwMDATGCFywwghco
|
|
||||||
# BgkqhkiG9w0BBwKgghcZMIIXFQIBAzEPMA0GCWCGSAFlAwQCAQUAMHgGCyqGSIb3
|
|
||||||
# DQEJEAEEoGkEZzBlAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgDHAE
|
|
||||||
# wrb/OjfkdGEAR/N6/5LxwnpqnhSdUI5gfWTSXKECEQDkKzKdiKykh3cqy0kBK32H
|
|
||||||
# GA8yMDIzMDgyNDE0NTcyOFqgghMJMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
|
|
||||||
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
|
|
||||||
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
|
|
||||||
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
|
|
||||||
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
|
|
||||||
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
|
|
||||||
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
|
|
||||||
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
|
|
||||||
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
|
|
||||||
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
|
|
||||||
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
|
|
||||||
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
|
|
||||||
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
|
|
||||||
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
|
|
||||||
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
|
|
||||||
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
|
|
||||||
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
|
|
||||||
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
|
|
||||||
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
|
|
||||||
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
|
|
||||||
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
|
|
||||||
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
|
|
||||||
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
|
|
||||||
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
|
|
||||||
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
|
|
||||||
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
|
|
||||||
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
|
|
||||||
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
|
|
||||||
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
|
|
||||||
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
|
|
||||||
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
|
|
||||||
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
|
|
||||||
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
|
|
||||||
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
|
|
||||||
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
|
|
||||||
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
|
|
||||||
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIGrjCCBJagAwIBAgIQBzY3tyRU
|
|
||||||
# fNhHrP0oZipeWzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE
|
|
||||||
# ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD
|
|
||||||
# VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIwMzIzMDAwMDAwWhcN
|
|
||||||
# MzcwMzIyMjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs
|
|
||||||
# IEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEy
|
|
||||||
# NTYgVGltZVN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
|
|
||||||
# AgEAxoY1BkmzwT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCwzIP5WvYRoUQVQl+k
|
|
||||||
# iPNo+n3znIkLf50fng8zH1ATCyZzlm34V6gCff1DtITaEfFzsbPuK4CEiiIY3+va
|
|
||||||
# PcQXf6sZKz5C3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ7Gnf2ZCHRgB720RB
|
|
||||||
# idx8ald68Dd5n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7QKxfst5Kfc71ORJn
|
|
||||||
# 7w6lY2zkpsUdzTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/tePc5OsLDnipUjW8LAx
|
|
||||||
# E6lXKZYnLvWHpo9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCYOjgRs/b2nuY7W+yB
|
|
||||||
# 3iIU2YIqx5K/oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9KoRxrOMUp88qqlnNC
|
|
||||||
# aJ+2RrOdOqPVA+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6dSgkQe1CvwWcZklS
|
|
||||||
# UPRR8zZJTYsg0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM1+mYSlg+0wOI/rOP
|
|
||||||
# 015LdhJRk8mMDDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbCdLI/Hgl27KtdRnXi
|
|
||||||
# YKNYCQEoAA6EVO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbECAwEAAaOCAV0wggFZ
|
|
||||||
# MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1NhS9zKXaaL3WMaiCP
|
|
||||||
# nshvMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQE
|
|
||||||
# AwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYB
|
|
||||||
# BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0
|
|
||||||
# cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5j
|
|
||||||
# cnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
|
|
||||||
# Z2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJ
|
|
||||||
# YIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7ZvmKlEIgF+ZtbYIULh
|
|
||||||
# sBguEE0TzzBTzr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI2AvlXFvXbYf6hCAl
|
|
||||||
# NDFnzbYSlm/EUExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/tydBTX/6tPiix6q4XN
|
|
||||||
# Q1/tYLaqT5Fmniye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVPulr3qRCyXen/KFSJ
|
|
||||||
# 8NWKcXZl2szwcqMj+sAngkSumScbqyQeJsG33irr9p6xeZmBo1aGqwpFyd/EjaDn
|
|
||||||
# mPv7pp1yr8THwcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc6UsCUqc3fpNTrDsd
|
|
||||||
# CEkPlM05et3/JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3cHXg65J6t5TRxktcm
|
|
||||||
# a+Q4c6umAU+9Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0dKNPH+ejxmF/7K9h+
|
|
||||||
# 8kaddSweJywm228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZPJ/tgZxahZrrdVcA6
|
|
||||||
# KYawmKAr7ZVBtzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLeMt8EifAAzV3C+dAj
|
|
||||||
# fwAL5HYCJtnwZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDyDivl1vupL0QVSucT
|
|
||||||
# Dh3bNzgaoSv27dZ8/DCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJ
|
|
||||||
# KoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu
|
|
||||||
# YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg
|
|
||||||
# QXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1
|
|
||||||
# OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
|
|
||||||
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBS
|
|
||||||
# b290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+Rd
|
|
||||||
# SjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20d
|
|
||||||
# q7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7f
|
|
||||||
# gvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRA
|
|
||||||
# X7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raR
|
|
||||||
# mECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzU
|
|
||||||
# vK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2
|
|
||||||
# mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkr
|
|
||||||
# fsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaA
|
|
||||||
# sPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxf
|
|
||||||
# jT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEe
|
|
||||||
# xcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQF
|
|
||||||
# MAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaA
|
|
||||||
# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcB
|
|
||||||
# AQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggr
|
|
||||||
# BgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz
|
|
||||||
# dXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5k
|
|
||||||
# aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQK
|
|
||||||
# MAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3
|
|
||||||
# v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy
|
|
||||||
# 3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cn
|
|
||||||
# RNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3
|
|
||||||
# WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2
|
|
||||||
# zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGC
|
|
||||||
# A3YwggNyAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
|
|
||||||
# bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2
|
|
||||||
# IFRpbWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjANBglghkgBZQMEAgEF
|
|
||||||
# AKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8X
|
|
||||||
# DTIzMDgyNDE0NTcyOFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQUZvArMsLCyQ+C
|
|
||||||
# Xc6qisnGTxmcz0AwLwYJKoZIhvcNAQkEMSIEICXoYBY9n9CHBlb2ZPmyIOzhh93Z
|
|
||||||
# zUXRoskCDmMHyq3cMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEINL25G3tdCLM0dRA
|
|
||||||
# V2hBNm+CitpVmq4zFq9NGprUDHgoMA0GCSqGSIb3DQEBAQUABIICAFUzNs5f5wsA
|
|
||||||
# nHsLg2yauMwAyYAuQIL8+GKYnWW/AtSWnA/t+S4LbjIJaIpBzZaWTai8/I23tJJw
|
|
||||||
# W1CTYDV3hqPGG/8PEcs8RY12JQoYMRZHzHTkNvUJC9xMXfuZIxtCmoFP2xsQjLgP
|
|
||||||
# Pl45FYCo3NzWCwQ8A2SyR48lskuJ94Q7PADJHkTU7pEY0t/N6114Mo9aO+n6qSLJ
|
|
||||||
# huEu1DmWE7iarxtIKja66BQEHjdawlSbg82Fg8EfkfsAXDHLqH1pahvnWmOziFLp
|
|
||||||
# SOrFKfyUVdCoGR7k3bKkHO62AeWz/LbzN0HPkzV7xrh/PD+4rwzatBpSwzFUFgRN
|
|
||||||
# 8Zg+Kso4LgTktu3nW9rG3TkFUBM3WsP9atnUfvCGvAcDr4Qv5qSx1cyFhuTK4gRj
|
|
||||||
# FVEO93RAMu0S54vTXBQDjl/55MEgWkFinTMqbkM4DpqqMp2uBjp5sqHbns6cDOD+
|
|
||||||
# o+HyXhE9XKxu3myklhq338QMSKE8bcdQ0XogmOwqgrVkpiG3jH+2R9CJlPwsyJLM
|
|
||||||
# TYwlsno3hd4+w2SPtySgNXwZGSj2KRNJFMaePyru8QeVS33pcmXNh+CKrdW5fxk0
|
|
||||||
# C4wWetuCKJTCBDNCRzy5NjrGAkNH2F37JM1pi0n5x2esaTyLC4+gGzDfn5ki0BT4
|
|
||||||
# 8w/WVhUIJnuoyQ33wBhxukpdRjwb71K4
|
|
||||||
# SIG # End signature block
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# This file must be used with "source bin/activate" *from bash*
|
|
||||||
# you cannot run it directly
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
# reset old environment variables
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
|
||||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
|
||||||
export PATH
|
|
||||||
unset _OLD_VIRTUAL_PATH
|
|
||||||
fi
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
|
||||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
|
||||||
export PYTHONHOME
|
|
||||||
unset _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
|
||||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
|
||||||
export PS1
|
|
||||||
unset _OLD_VIRTUAL_PS1
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset VIRTUAL_ENV
|
|
||||||
unset VIRTUAL_ENV_PROMPT
|
|
||||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
|
||||||
# Self destruct!
|
|
||||||
unset -f deactivate
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# unset irrelevant variables
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
VIRTUAL_ENV="D:\dev\etc\SolorPower\crawler\venv_win"
|
|
||||||
export VIRTUAL_ENV
|
|
||||||
|
|
||||||
_OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
PATH="$VIRTUAL_ENV/Scripts:$PATH"
|
|
||||||
export PATH
|
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
|
||||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
|
||||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
|
||||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
|
||||||
unset PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
|
||||||
PS1="(venv_win) ${PS1:-}"
|
|
||||||
export PS1
|
|
||||||
VIRTUAL_ENV_PROMPT="(venv_win) "
|
|
||||||
export VIRTUAL_ENV_PROMPT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
fi
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
@echo off
|
|
||||||
|
|
||||||
rem This file is UTF-8 encoded, so we need to update the current code page while executing it
|
|
||||||
for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do (
|
|
||||||
set _OLD_CODEPAGE=%%a
|
|
||||||
)
|
|
||||||
if defined _OLD_CODEPAGE (
|
|
||||||
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
|
||||||
)
|
|
||||||
|
|
||||||
set VIRTUAL_ENV=D:\dev\etc\SolorPower\crawler\venv_win
|
|
||||||
|
|
||||||
if not defined PROMPT set PROMPT=$P$G
|
|
||||||
|
|
||||||
if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT%
|
|
||||||
if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PROMPT=%PROMPT%
|
|
||||||
set PROMPT=(venv_win) %PROMPT%
|
|
||||||
|
|
||||||
if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%
|
|
||||||
set PYTHONHOME=
|
|
||||||
|
|
||||||
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
|
||||||
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
|
||||||
|
|
||||||
set PATH=%VIRTUAL_ENV%\Scripts;%PATH%
|
|
||||||
set VIRTUAL_ENV_PROMPT=(venv_win)
|
|
||||||
|
|
||||||
:END
|
|
||||||
if defined _OLD_CODEPAGE (
|
|
||||||
"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
|
|
||||||
set _OLD_CODEPAGE=
|
|
||||||
)
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
@echo off
|
|
||||||
|
|
||||||
if defined _OLD_VIRTUAL_PROMPT (
|
|
||||||
set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
|
|
||||||
)
|
|
||||||
set _OLD_VIRTUAL_PROMPT=
|
|
||||||
|
|
||||||
if defined _OLD_VIRTUAL_PYTHONHOME (
|
|
||||||
set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%"
|
|
||||||
set _OLD_VIRTUAL_PYTHONHOME=
|
|
||||||
)
|
|
||||||
|
|
||||||
if defined _OLD_VIRTUAL_PATH (
|
|
||||||
set "PATH=%_OLD_VIRTUAL_PATH%"
|
|
||||||
)
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PATH=
|
|
||||||
|
|
||||||
set VIRTUAL_ENV=
|
|
||||||
set VIRTUAL_ENV_PROMPT=
|
|
||||||
|
|
||||||
:END
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +0,0 @@
|
||||||
home = C:\Users\haneu\AppData\Local\Programs\Python\Python311
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.11.5
|
|
||||||
executable = C:\Users\haneu\AppData\Local\Programs\Python\Python311\python.exe
|
|
||||||
command = C:\Users\haneu\AppData\Local\Programs\Python\Python311\python.exe -m venv D:\dev\etc\SolorPower\crawler\venv_win
|
|
||||||
|
|
@ -44,6 +44,13 @@ $env:DEBUG = "false"
|
||||||
|
|
||||||
`package-lock.json`을 저장소에 포함하고 CI와 새 환경에서는 `npm ci`를 사용한다.
|
`package-lock.json`을 저장소에 포함하고 CI와 새 환경에서는 `npm ci`를 사용한다.
|
||||||
|
|
||||||
|
API 주소와 현재 조회 업체는 Expo 공개 환경변수로 선택한다. 기본값은 운영 환경을 유지하며, 다른 환경에서는 `app/.env.example`을 `app/.env.local`로 복사해 값을 바꾼다. 이 값은 앱 번들에 포함되는 공개 설정이므로 인증 토큰이나 비밀번호를 넣지 않는다.
|
||||||
|
|
||||||
|
| 환경변수 | 용도 | 기본값 |
|
||||||
|
|---|---|---|
|
||||||
|
| `EXPO_PUBLIC_API_BASE_URL` | FastAPI/Nginx 기본 URL | `https://solorpower.dadot.net` |
|
||||||
|
| `EXPO_PUBLIC_COMPANY_ID` | 대시보드와 비교 화면의 업체 ID | `1` |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd app
|
cd app
|
||||||
npm ci
|
npm ci
|
||||||
|
|
@ -52,6 +59,8 @@ npm run test:ci
|
||||||
|
|
||||||
현재 `test:ci`는 Expo 웹 export를 수행하여 의존성, 번들링, 정적 산출물 생성을 함께 검사한다. 출력 디렉터리는 `app/dist/`이며 Git에서 제외된다.
|
현재 `test:ci`는 Expo 웹 export를 수행하여 의존성, 번들링, 정적 산출물 생성을 함께 검사한다. 출력 디렉터리는 `app/dist/`이며 Git에서 제외된다.
|
||||||
|
|
||||||
|
앱의 네트워크 호출은 `services/solarApi.js`, 환경 선택은 `config/appConfig.js`를 통해 수행한다. 화면에 API URL이나 업체 ID를 직접 작성하지 않는다.
|
||||||
|
|
||||||
## DB migration
|
## DB migration
|
||||||
|
|
||||||
통계 쓰기 일관성 migration은 독립 PostgreSQL 15 DB에서 다음 계약 테스트로 확인한다.
|
통계 쓰기 일관성 migration은 독립 PostgreSQL 15 DB에서 다음 계약 테스트로 확인한다.
|
||||||
|
|
|
||||||
60
docs/repository_structure.md
Normal file
60
docs/repository_structure.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# 저장소 및 앱 코드 구조
|
||||||
|
|
||||||
|
작성일: 2026-08-07
|
||||||
|
|
||||||
|
이 문서는 8단계 저장소 정리 이후 유지해야 할 디렉터리 책임과 산출물 관리 기준을 정의한다.
|
||||||
|
|
||||||
|
## 최상위 디렉터리
|
||||||
|
|
||||||
|
| 경로 | 책임 |
|
||||||
|
|---|---|
|
||||||
|
| `crawler/` | 외부 발전 사이트 수집, 알림, 일·월 통계 저장과 백필 |
|
||||||
|
| `api_server/` | 앱용 FastAPI 조회·업로드 API |
|
||||||
|
| `app/` | Expo 기반 모바일·웹 대시보드 |
|
||||||
|
| `supabase/` | PostgreSQL migration과 DB 계약 테스트 |
|
||||||
|
| `deploy/` | Nginx 등 버전 관리 대상 배포 설정 |
|
||||||
|
| `docs/` | 운영, 개발, 개선 단계와 구조 문서 |
|
||||||
|
| `sql/` | 초기 스키마 및 과거 SQL 참고 자료 |
|
||||||
|
| `tools/` | 프로젝트 외부 입력을 변환하는 독립 도구 |
|
||||||
|
|
||||||
|
## 앱 내부 경계
|
||||||
|
|
||||||
|
| 경로 | 책임 |
|
||||||
|
|---|---|
|
||||||
|
| `config/appConfig.js` | 환경별 API 기본 URL과 현재 업체 선택 |
|
||||||
|
| `services/solarApi.js` | HTTP 경로, 쿼리 구성, 오류 응답 처리 |
|
||||||
|
| `hooks/usePlantStats.js` | 상세 통계 조회 상태와 새로고침 수명주기 |
|
||||||
|
| `utils/statsChart.js` | API 통계를 차트 데이터로 변환하는 순수 로직 |
|
||||||
|
| `components/` | 업로드 모달과 기간 선택 등 재사용 UI |
|
||||||
|
| `screens/` | 화면 배치와 사용자 상호작용 조합 |
|
||||||
|
|
||||||
|
현재 앱은 한 업체를 선택해 보여 주지만 업체 ID를 화면이나 URL에 직접 하드코딩하지 않는다. 배포 환경은 `EXPO_PUBLIC_COMPANY_ID`로 업체를 선택하고, 대시보드가 받은 각 발전소의 `company_id`를 변경 API에 다시 사용한다. 비교 API는 선택적인 `company_id` 쿼리를 받아 같은 업체의 발전소만 반환한다. 향후 업체 선택 UI를 추가할 때는 이 설정값을 앱 상태로 승격하고 서비스 함수에 전달하는 경계를 유지한다.
|
||||||
|
|
||||||
|
## 생성 파일 관리
|
||||||
|
|
||||||
|
다음 파일은 Git에 추가하지 않는다.
|
||||||
|
|
||||||
|
- Python 가상환경: `.venv/`, `venv/`, `venv_win/`
|
||||||
|
- Node/Expo 산출물: `node_modules/`, `.expo/`, `dist/`, `build/`
|
||||||
|
- 런타임 상태와 출력: `*.db`, `*.db-journal`, `*.log`
|
||||||
|
- 로컬 환경과 인증정보: `.env`, `.env.*` (`.env.example` 제외), `secrets.json`
|
||||||
|
- 서버 배포 백업과 staging: `deploy_backups/`, `deploy_staging/`
|
||||||
|
- 로컬 Supabase CLI 바이너리: `supabase.exe`, `supabase-go.exe`
|
||||||
|
|
||||||
|
가상환경은 각 하위 프로젝트의 고정 `requirements.txt`로, 앱 의존성은 `package-lock.json`과 `npm ci`로 재생성한다.
|
||||||
|
|
||||||
|
## 보관 정책
|
||||||
|
|
||||||
|
- 실행 코드와 같은 디렉터리에 `*_old.py`, `*.backup` 사본을 두지 않는다. 이전 버전은 Git 이력에서 복구한다.
|
||||||
|
- 완료된 일회성 데이터 보정 스크립트는 삭제 목적, 영향 데이터, 실행 주의사항이 있는 경우에만 `crawler/scripts_archive/`에 둔다.
|
||||||
|
- `scripts_archive/`의 코드는 정기 실행 진입점이 아니며, 다시 실행하기 전에 현재 DB 스키마와 KST 처리 정책을 재검토한다.
|
||||||
|
- 운영 배포 전 백업은 서버의 `deploy_backups/`에 보존하되 저장소에는 포함하지 않는다.
|
||||||
|
|
||||||
|
## 8단계에서 제거한 추적 파일
|
||||||
|
|
||||||
|
- `crawler/venv_win/`: 개발 PC에서 생성된 Windows 가상환경과 실행 파일
|
||||||
|
- `app/server.log`: Expo 개발 서버 런타임 로그
|
||||||
|
- `crawler/crawlers/cmsolar_old.py`, `cmsolar_old2.py`: 현재 라우팅에서 참조되지 않는 구형 구현
|
||||||
|
- `crawler/crawlers/sun_wms_old.py`, `sun_wms.py.backup`: 현재 구현과 중복된 사본
|
||||||
|
|
||||||
|
모든 소스 사본은 삭제 전 Git 이력에 존재하므로 필요하면 해당 커밋에서 복구할 수 있다.
|
||||||
Loading…
Reference in New Issue
Block a user