diff --git a/README.md b/README.md
new file mode 100644
index 0000000..de9bfd6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,183 @@
+# ⚡ 전기차 충전소 관리 시스템 — 라즈베리파이 / 아두이노 모듈
+
+> **캡스톤디자인 (2025 봄학기) — 팀 QuarterBack**
+>
+> OCPP 2.0.1 프로토콜 기반의 전기차 충전소 IoT 클라이언트입니다.
+> 라즈베리파이가 충전소 단말(EVSE) 역할을 수행하며, 아두이노를 통해 실제 충전기 플러그 탈착을 감지하고
+> WebSocket으로 백엔드 OCPP 서버와 실시간 통신합니다.
+
+
+
+## 📌 담당 역할
+
+본 레포지토리는 팀 프로젝트에서 **라즈베리파이 & 아두이노 파트**를 담당한 코드입니다.
+
+- 아두이노로 충전기 케이블 탈착 신호를 감지하여 시리얼 통신으로 라즈베리파이에 전송
+- 라즈베리파이에서 OCPP 2.0.1 메시지를 생성하여 백엔드 서버로 전송
+- 충전 시작 → 전력 측정 → 충전 종료 전 과정을 시나리오로 구현
+- tkinter 기반 GUI 대시보드로 3개 충전기(EVSE) 상태를 실시간 시각화
+
+
+
+## 🏗️ 시스템 구성도
+
+```
+[ 아두이노 ]
+ └─ 충전기 케이블 탈착 감지 (전류/전압 측정)
+ └─ 시리얼 통신 (Serial, Baud Rate: 2400) ──→
+
+[ 라즈베리파이 4B ] [ 백엔드 서버 ]
+ └─ Python GUI 앱 실행 └─ Java Spring Boot
+ └─ OCPP 2.0.1 메시지 생성/파싱 WebSocket └─ OCPP WebSocket Handler
+ └─ WebSocket 클라이언트 ←────────────────→ └─ MySQL / MongoDB / Redis
+ └─ 충전 트랜잭션 관리 └─ REST API
+ └─ 실시간 전력 모니터링 GUI └─ 관리자 웹 대시보드 (React)
+```
+
+
+
+## ✨ 주요 기능
+
+| 기능 | 설명 |
+|---|---|
+| 🔌 **충전기 탈착 감지** | 아두이노에서 케이블 연결/분리 신호를 시리얼로 수신 |
+| 📡 **OCPP 통신** | BootNotification, Authorize, TransactionEvent, MeterValue 등 메시지 송수신 |
+| 🔑 **사용자 인증** | idToken 기반 `Authorize` 요청으로 충전 인증 처리 |
+| ⚡ **실시간 전력 모니터링** | 시리얼 포트로 전력값(W) 수신 및 그래프 시각화 |
+| 💰 **충전 요금 계산** | 서버로부터 수신한 단가(원/Wh) 기반 실시간 요금 계산 |
+| 🔄 **자동 재시도** | 메시지 전송 실패 시 최대 3회 자동 재시도 (큐 기반 순차 처리) |
+| 🖥️ **GUI 대시보드** | tkinter 기반 3개 EVSE 동시 모니터링 + 로그 뷰어 |
+| 🔧 **수동 모드** | 시리얼 포트 없이 전력값을 직접 입력하는 테스트 모드 지원 |
+
+
+
+## 🛠️ 기술 스택
+
+### 라즈베리파이 (이 레포)
+
+| 항목 | 내용 |
+|---|---|
+| **언어** | Python 3.x |
+| **GUI** | tkinter, ttk |
+| **WebSocket** | `websockets >= 10.0` |
+| **시리얼 통신** | `pyserial >= 3.5` |
+| **비동기 처리** | `asyncio`, `threading` |
+| **프로토콜** | OCPP 2.0.1 |
+| **하드웨어** | Raspberry Pi 4B + Arduino |
+
+### 백엔드 서버 (연동 시스템)
+
+| 항목 | 내용 |
+|---|---|
+| **언어 / 프레임워크** | Java 17, Spring Boot 3.3.1 |
+| **통신** | WebSocket (OCPP 핸들러), REST API |
+| **인증** | Spring Security + JWT |
+| **DB** | MySQL, MongoDB, Redis |
+| **문서화** | Swagger (springdoc-openapi 2.0.2) |
+| **빌드** | Gradle |
+
+
+
+## 📁 파일 구조
+
+```
+rasberyPi/
+├── main.py # 진입점 — GUI 앱 실행
+├── gui_app.py # tkinter 메인 애플리케이션 (3 EVSE 관리)
+├── gui_client.py # OCPP 클라이언트 — 메시지 생성 및 충전 시나리오
+├── ocpp_comm.py # WebSocket 통신 모듈 (메시지 큐, 재시도 로직)
+├── ocpp_message.py # 메시지 ID / 타임스탬프 / 트랜잭션 ID 생성 유틸
+├── charger_windows.py # 충전기별 팝업 창 (로그인, 충전 제어)
+├── visual_dashboard.py # 전력 미터, 상태 시각화 위젯
+├── enums.py # EventType, TriggerReason, ConnectorStatus 열거형
+├── utils.py # 설정 저장/불러오기, 전력값 포맷팅 유틸
+├── requirements.txt # 의존성 목록
+└── logs/ # 실행 로그 저장 디렉토리
+```
+
+
+
+## ⚙️ OCPP 메시지 흐름
+
+충전 한 사이클의 시나리오입니다.
+
+```
+라즈베리파이 백엔드 서버
+ │ │
+ │── BootNotification ──────────────────→ │ (부팅 시 1회)
+ │← BootNotification Response ─────────── │
+ │ │
+ │── Authorize (idToken) ───────────────→ │ (사용자 인증)
+ │← Authorize Response ─────────────────── │
+ │ │
+ │ [아두이노: 케이블 연결 감지] │
+ │── TransactionEvent (Started) ─────────→ │ (충전 시작)
+ │← TransactionEvent Response ──────────── │ (transactionId 수신)
+ │ │
+ │── MeterValue (전력 데이터 주기 전송) ──→ │ (충전 중)
+ │ │
+ │ [아두이노: 케이블 분리 감지] │
+ │── TransactionEvent (Ended) ────────────→ │ (충전 종료)
+ │← TransactionEvent Response ──────────── │
+```
+
+
+
+## 🚀 실행 방법
+
+### 1. 의존성 설치
+
+```bash
+pip install -r requirements.txt
+```
+
+### 2. 실행
+
+```bash
+python main.py
+```
+
+### 3. GUI 설정
+
+| 항목 | 기본값 | 설명 |
+|---|---|---|
+| WebSocket URL | `ws://localhost:8080/ocpp` | 백엔드 서버 주소 |
+| 시리얼 포트 | `/dev/ttyUSB0` (라즈베리파이 자동) | 아두이노 연결 포트 |
+| Baud Rate | `2400` | 시리얼 통신 속도 |
+| 수동 모드 | 체크 해제 | 시리얼 없이 수동 전력 입력 |
+
+> 라즈베리파이 환경에서는 시리얼 포트가 `/dev/ttyUSB0`으로 자동 설정됩니다.
+> Windows 테스트 환경에서는 `COM3` 등으로 직접 입력하거나 수동 모드를 사용하세요.
+
+
+
+## 👥 팀원
+
+| 이름 | 역할 |
+|---|---|
+| 마영창 | 라즈베리파이 / 아두이노 **(본 레포)** |
+| 이동호 | 백엔드 (Spring Boot, OCPP 서버) |
+| 김태원 | 팀원 |
+| 윤덕규 | 팀원 |
+
+
+
+## 🔗 전체 프로젝트 확인
+
+본 레포는 전체 시스템의 라즈베리파이 파트입니다.
+프로젝트 전체 구성 및 다른 모듈은 아래 GitHub 조직 페이지에서 확인하세요.
+
+> 👉 **[https://github.com/Capstone-QuarterBack](https://github.com/Capstone-QuarterBack)**
+
+| 레포지토리 | 설명 |
+|---|---|
+| [rasberyPi](https://github.com/Capstone-QuarterBack/rasberyPi) | 🔴 라즈베리파이 / 아두이노 클라이언트 **(현재 레포)** |
+| [backend](https://github.com/Capstone-QuarterBack/backend) | Spring Boot 기반 OCPP 서버 |
+| [frontend](https://github.com/Capstone-QuarterBack/frontend) | 관리자 웹 대시보드 (React/TypeScript) |
+| [front](https://github.com/Capstone-QuarterBack/front) | 프론트엔드 (TypeScript) |
+
+
+
+---
+
+> 세종대학교 컴퓨터공학과 | 2025 캡스톤디자인 | 팀 QuarterBack (컴1-13)
diff --git a/__pycache__/charger_windows.cpython-311.pyc b/__pycache__/charger_windows.cpython-311.pyc
new file mode 100644
index 0000000..03c00e9
Binary files /dev/null and b/__pycache__/charger_windows.cpython-311.pyc differ
diff --git a/__pycache__/charger_windows.cpython-313.pyc b/__pycache__/charger_windows.cpython-313.pyc
index e37a0ab..056482d 100644
Binary files a/__pycache__/charger_windows.cpython-313.pyc and b/__pycache__/charger_windows.cpython-313.pyc differ
diff --git a/__pycache__/charger_windows.cpython-38.pyc b/__pycache__/charger_windows.cpython-38.pyc
new file mode 100644
index 0000000..1f5b63f
Binary files /dev/null and b/__pycache__/charger_windows.cpython-38.pyc differ
diff --git a/__pycache__/enums.cpython-311.pyc b/__pycache__/enums.cpython-311.pyc
new file mode 100644
index 0000000..06be7ac
Binary files /dev/null and b/__pycache__/enums.cpython-311.pyc differ
diff --git a/__pycache__/enums.cpython-313.pyc b/__pycache__/enums.cpython-313.pyc
index 01ff6d1..cdbb50a 100644
Binary files a/__pycache__/enums.cpython-313.pyc and b/__pycache__/enums.cpython-313.pyc differ
diff --git a/__pycache__/enums.cpython-38.pyc b/__pycache__/enums.cpython-38.pyc
new file mode 100644
index 0000000..bd2d316
Binary files /dev/null and b/__pycache__/enums.cpython-38.pyc differ
diff --git a/__pycache__/gui_app.cpython-311.pyc b/__pycache__/gui_app.cpython-311.pyc
new file mode 100644
index 0000000..052ac68
Binary files /dev/null and b/__pycache__/gui_app.cpython-311.pyc differ
diff --git a/__pycache__/gui_app.cpython-313.pyc b/__pycache__/gui_app.cpython-313.pyc
index 9c42500..e36e6fa 100644
Binary files a/__pycache__/gui_app.cpython-313.pyc and b/__pycache__/gui_app.cpython-313.pyc differ
diff --git a/__pycache__/gui_app.cpython-38.pyc b/__pycache__/gui_app.cpython-38.pyc
new file mode 100644
index 0000000..b87bf22
Binary files /dev/null and b/__pycache__/gui_app.cpython-38.pyc differ
diff --git a/__pycache__/gui_client.cpython-311.pyc b/__pycache__/gui_client.cpython-311.pyc
new file mode 100644
index 0000000..985715e
Binary files /dev/null and b/__pycache__/gui_client.cpython-311.pyc differ
diff --git a/__pycache__/gui_client.cpython-313.pyc b/__pycache__/gui_client.cpython-313.pyc
index 7eb42c1..cc0c8d8 100644
Binary files a/__pycache__/gui_client.cpython-313.pyc and b/__pycache__/gui_client.cpython-313.pyc differ
diff --git a/__pycache__/gui_client.cpython-38.pyc b/__pycache__/gui_client.cpython-38.pyc
new file mode 100644
index 0000000..0c101cb
Binary files /dev/null and b/__pycache__/gui_client.cpython-38.pyc differ
diff --git a/__pycache__/ocpp_comm.cpython-311.pyc b/__pycache__/ocpp_comm.cpython-311.pyc
new file mode 100644
index 0000000..fd8d77d
Binary files /dev/null and b/__pycache__/ocpp_comm.cpython-311.pyc differ
diff --git a/__pycache__/ocpp_comm.cpython-313.pyc b/__pycache__/ocpp_comm.cpython-313.pyc
index ac5f8ad..dd2ab51 100644
Binary files a/__pycache__/ocpp_comm.cpython-313.pyc and b/__pycache__/ocpp_comm.cpython-313.pyc differ
diff --git a/__pycache__/ocpp_comm.cpython-38.pyc b/__pycache__/ocpp_comm.cpython-38.pyc
new file mode 100644
index 0000000..36dc3d8
Binary files /dev/null and b/__pycache__/ocpp_comm.cpython-38.pyc differ
diff --git a/__pycache__/ocpp_message.cpython-311.pyc b/__pycache__/ocpp_message.cpython-311.pyc
new file mode 100644
index 0000000..0e8f073
Binary files /dev/null and b/__pycache__/ocpp_message.cpython-311.pyc differ
diff --git a/__pycache__/ocpp_message.cpython-313.pyc b/__pycache__/ocpp_message.cpython-313.pyc
index c89d088..1742392 100644
Binary files a/__pycache__/ocpp_message.cpython-313.pyc and b/__pycache__/ocpp_message.cpython-313.pyc differ
diff --git a/__pycache__/ocpp_message.cpython-38.pyc b/__pycache__/ocpp_message.cpython-38.pyc
new file mode 100644
index 0000000..442e6c3
Binary files /dev/null and b/__pycache__/ocpp_message.cpython-38.pyc differ
diff --git a/__pycache__/visual_dashboard.cpython-311.pyc b/__pycache__/visual_dashboard.cpython-311.pyc
new file mode 100644
index 0000000..137ce46
Binary files /dev/null and b/__pycache__/visual_dashboard.cpython-311.pyc differ
diff --git a/__pycache__/visual_dashboard.cpython-313.pyc b/__pycache__/visual_dashboard.cpython-313.pyc
index 4fd6d14..87a1a3d 100644
Binary files a/__pycache__/visual_dashboard.cpython-313.pyc and b/__pycache__/visual_dashboard.cpython-313.pyc differ
diff --git a/__pycache__/visual_dashboard.cpython-38.pyc b/__pycache__/visual_dashboard.cpython-38.pyc
new file mode 100644
index 0000000..01d449c
Binary files /dev/null and b/__pycache__/visual_dashboard.cpython-38.pyc differ
diff --git a/charger_windows.py b/charger_windows.py
index 1e3f626..c9ec57d 100644
--- a/charger_windows.py
+++ b/charger_windows.py
@@ -145,8 +145,8 @@ def handle_login_result(self, success, message):
self.status_var.set(message)
if success:
- # 잠시 후 창 닫고 다음 화면으로 이동
- self.after(1000, lambda: self.on_success())
+ # 지연 없이 즉시 다음 화면으로 이동
+ self.on_success()
else:
# 오류 메시지 표시
self.status_var.set(f"오류: {message}")
@@ -162,7 +162,7 @@ class ChargingWindow(tk.Toplevel):
def __init__(self, parent, charger_id, ocpp_client, event_loop):
super().__init__(parent)
self.title(f"충전기 {charger_id}")
- self.geometry("400x450") # 창 크기를 더 작게 조정
+ self.geometry("400x500")
self.resizable(False, False)
self.charger_id = charger_id
self.ocpp_client = ocpp_client
@@ -229,6 +229,17 @@ def create_widgets(self):
price_value = ttk.Label(price_frame, textvariable=self.price_var)
price_value.pack(side=tk.LEFT)
+ # Total price information
+ total_price_frame = ttk.Frame(main_frame)
+ total_price_frame.pack(fill=tk.X, pady=5)
+
+ total_price_label = ttk.Label(total_price_frame, text="총 금액:", width=10, anchor="w")
+ total_price_label.pack(side=tk.LEFT)
+
+ self.total_price_var = tk.StringVar(value="-")
+ self.total_price_value = ttk.Label(total_price_frame, textvariable=self.total_price_var, font=("Arial", 12, "bold"), foreground="#4CAF50")
+ self.total_price_value.pack(side=tk.LEFT)
+
# Current power display
current_power_frame = ttk.Frame(main_frame)
current_power_frame.pack(fill=tk.X, pady=10)
@@ -266,7 +277,7 @@ def create_widgets(self):
self.power_entry = ttk.Entry(power_frame)
self.power_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
- self.power_entry.insert(0, "3000")
+ self.power_entry.insert(0, "0") # 기본값을 0으로 변경
# Apply button
apply_button = ttk.Button(
@@ -278,7 +289,7 @@ def create_widgets(self):
else:
# 시리얼 포트 사용 중일 때는 Entry를 만들지만 표시하지 않음 (다른 메서드에서 참조할 때 오류 방지)
self.power_entry = ttk.Entry(main_frame)
- self.power_entry.insert(0, "3000")
+ self.power_entry.insert(0, "0") # 기본값을 0으로 변경
# Buttons frame
buttons_frame = ttk.Frame(main_frame)
@@ -367,8 +378,8 @@ def check_power_and_update_status(self):
elif not connected and self.charging and not self.manual_mode:
self.stop_charging_auto()
- # 다음 확인 예약 (500ms 마다)
- self.power_check_timer = self.after(500, self.check_power_and_update_status)
+ # 다음 확인 예약 (500ms에서 1000ms로 증가 - GUI 업데이트 시간 늘림)
+ self.power_check_timer = self.after(1000, self.check_power_and_update_status)
def apply_manual_power(self):
"""수동 전력값 적용"""
@@ -418,15 +429,29 @@ def start_charging_auto(self):
# 트랜잭션이 아직 시작되지 않았으면 시작
if not self.transaction_started:
- # 기본 전력값 설정 (실제 측정값 사용)
- power = max(3000, self.last_power_value) # 최소 3000W 또는 현재 측정값
+ # 기본 전력값 설정 (0W로 시작)
+ power = 0 # 기본값을 0W로 변경
- # 트랜잭션 시작 이벤트 전송
- asyncio.run_coroutine_threadsafe(
- self.ocpp_client.start_charging(self.charger_id, power),
- self.event_loop
- )
- self.transaction_started = True
+ # 트랜잭션 시작 이벤트 전송 시 예외 처리 추가
+ try:
+ # 트랜잭션 시작 이벤트를 별도의 함수로 분리하여 실행
+ def start_transaction_async():
+ try:
+ asyncio.run_coroutine_threadsafe(
+ self.ocpp_client.start_charging(self.charger_id, power),
+ self.event_loop
+ )
+ self.transaction_started = True
+ except Exception as e:
+ print(f"자동 트랜잭션 시작 오류: {e}")
+ self.update_status("충전 오류")
+
+ # 약간의 지연을 두어 GUI 업데이트 후 트랜잭션 시작
+ self.after(100, start_transaction_async)
+ except Exception as e:
+ print(f"자동 충전 시작 예약 오류: {e}")
+ self.update_status("충전 오류")
+ return
# 버튼 상태 업데이트 (수동 중지만 가능)
self.start_button.config(state=tk.DISABLED)
@@ -440,12 +465,26 @@ def stop_charging_auto(self):
# 트랜잭션이 시작되었으면 종료
if self.transaction_started:
- # 트랜잭션 종료 이벤트 전송
- asyncio.run_coroutine_threadsafe(
- self.ocpp_client.stop_charging(self.charger_id),
- self.event_loop
- )
- self.transaction_started = False
+ # 트랜잭션 종료 이벤트 전송 시 예외 처리 추가
+ try:
+ # 트랜잭션 종료 이벤트를 별도의 함수로 분리하여 실행
+ def stop_transaction_async():
+ try:
+ asyncio.run_coroutine_threadsafe(
+ self.ocpp_client.stop_charging(self.charger_id),
+ self.event_loop
+ )
+ self.transaction_started = False
+ except Exception as e:
+ print(f"트랜잭션 종료 오류: {e}")
+ self.update_status("중지 오류")
+
+ # 약간의 지연을 두어 GUI 업데이트 후 트랜잭션 종료
+ self.after(100, stop_transaction_async)
+ except Exception as e:
+ print(f"충전 중지 예약 오류: {e}")
+ self.update_status("중지 오류")
+ return
# 버튼 상태 업데이트 (수동 시작만 가능)
self.start_button.config(state=tk.NORMAL)
@@ -461,8 +500,8 @@ def start_charging_manually(self):
if not self.using_serial:
# 입력된 전력값 가져오기
power = float(self.power_entry.get())
- if power <= 0:
- messagebox.showerror("입력 오류", "전력은 양수여야 합니다")
+ if power < 0: # 0 이상의 값만 허용
+ messagebox.showerror("입력 오류", "전력은 0 이상이어야 합니다")
return
# 전력값 설정
@@ -474,7 +513,7 @@ def start_charging_manually(self):
# 전압/전류 값도 업데이트 (로그에 사용되는 값)
voltage = 220.0
- current = power / voltage
+ current = power / voltage if power > 0 else 0.0 # 0으로 나누기 방지
self.ocpp_client.load3_mv[idx*2] = voltage
self.ocpp_client.load3_mv[idx*2+1] = current
@@ -484,20 +523,34 @@ def start_charging_manually(self):
# 케이블 연결 상태 설정
self.ocpp_client.cable_connected[idx] = True
else:
- # 시리얼 포트 사용 중인 경우 실제 측정값 사용
- power = max(3000, self.last_power_value) # 최소 3000W 또는 현재 측정값
+ # 시리얼 포트 사용 중인 경우 초기 전력값 0으로 시작
+ power = 0 # 기본값을 0W로 변경
# 충전 시작
self.charging = True
self.update_status("충전 중 (수동 모드)")
self.update_connection_status(True)
- # 트랜잭션 시작 이벤트 전송
- asyncio.run_coroutine_threadsafe(
- self.ocpp_client.start_charging(self.charger_id, power),
- self.event_loop
- )
- self.transaction_started = True
+ # 트랜잭션 시작 이벤트 전송 시 예외 처리 추가
+ try:
+ # 트랜잭션 시작 이벤트를 별도의 함수로 분리하여 실행
+ def start_transaction_async():
+ try:
+ asyncio.run_coroutine_threadsafe(
+ self.ocpp_client.start_charging(self.charger_id, power),
+ self.event_loop
+ )
+ self.transaction_started = True
+ except Exception as e:
+ print(f"트랜잭션 시작 오류: {e}")
+ messagebox.showerror("충전 시작 오류", f"충전 시작 중 오류가 발생했습니다:\n{str(e)}")
+
+ # 약간의 지연을 두어 GUI 업데이트 후 트랜잭션 시작
+ self.after(100, start_transaction_async)
+ except Exception as e:
+ print(f"충전 시작 예약 오류: {e}")
+ messagebox.showerror("충전 시작 오류", f"충전 시작 중 오류가 발생했습니다:\n{str(e)}")
+ return
# 버튼 상태 업데이트
self.start_button.config(state=tk.DISABLED)
@@ -505,6 +558,9 @@ def start_charging_manually(self):
except ValueError:
messagebox.showerror("입력 오류", "유효한 숫자를 입력하세요")
+ except Exception as e:
+ print(f"충전 시작 중 예상치 못한 오류: {e}")
+ messagebox.showerror("충전 시작 오류", f"충전 시작 중 오류가 발생했습니다:\n{str(e)}")
def stop_charging_manually(self):
"""수동 충전 중지"""
@@ -525,3 +581,28 @@ def on_closing(self):
self.destroy()
else:
self.destroy()
+
+ def update_total_price(self, total_price):
+ """총 금액 정보 업데이트"""
+ try:
+ if total_price is not None:
+ # 금액 유효성 확인
+ if isinstance(total_price, (int, float)) and total_price >= 0:
+ self.total_price_var.set(f"{total_price}원")
+
+ # 충전 상태를 '완료'로 변경하고 버튼 상태 업데이트
+ self.charging = False
+ self.manual_mode = False
+ self.update_status("충전 완료")
+
+ # 버튼 상태 업데이트
+ self.start_button.config(state=tk.NORMAL)
+ self.stop_button.config(state=tk.DISABLED)
+ else:
+ print(f"금액 형식 오류: {total_price}")
+ self.total_price_var.set("금액 오류")
+ else:
+ self.total_price_var.set("-")
+ except Exception as e:
+ print(f"총 금액 업데이트 오류: {e}")
+ self.total_price_var.set("업데이트 오류")
diff --git a/gui_app.py b/gui_app.py
index 777a7e1..299e644 100644
--- a/gui_app.py
+++ b/gui_app.py
@@ -78,18 +78,21 @@ def create_widgets(self):
self.ws_entry = ttk.Entry(ws_frame)
self.ws_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
- self.ws_entry.insert(0, "ws://localhost:8080/ocpp")
+ self.ws_entry.insert(0, "ws://172.23.141.144:8080/ocpp")
# Serial port
+ #"/dev/ttyUSB0"
serial_frame = ttk.Frame(left_frame)
serial_frame.pack(fill=tk.X, pady=5)
serial_label = ttk.Label(serial_frame, text="시리얼 포트:")
serial_label.pack(side=tk.LEFT, padx=(0, 5))
+
self.serial_entry = ttk.Entry(serial_frame)
self.serial_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
-
+ self.serial_entry.insert(0, "/dev/ttyUSB0")
+
# Use serial checkbox
self.use_serial_var = tk.BooleanVar(value=False)
use_serial_check = ttk.Checkbutton(
@@ -268,6 +271,19 @@ def update_power_display(self, charger_id, power_value):
if charger_id in self.charger_windows and self.charger_windows[charger_id].winfo_exists():
self.charger_windows[charger_id].update_power_display(power_value)
+ def update_total_price(self, charger_id, total_price):
+ """충전 완료 후 총 금액 정보 업데이트"""
+ if 1 <= charger_id <= NUM_EVSE:
+ # 로그에 기록
+ self.log(f"충전기 {charger_id}: 총 금액 {total_price}원")
+
+ # 시각화 대시보드에 총 금액 업데이트
+ self.charger_visuals[charger_id - 1].update_total_price(total_price)
+
+ # 충전기 창이 열려 있으면 금액 정보 업데이트
+ if charger_id in self.charger_windows and self.charger_windows[charger_id].winfo_exists():
+ self.charger_windows[charger_id].update_total_price(total_price)
+
def toggle_connection(self):
"""연결/해제 토글"""
if not self.ocpp_client or not self.ocpp_client.running:
@@ -333,6 +349,11 @@ def open_charger(self, charger_id):
messagebox.showinfo("충전기 사용중", f"충전기 {charger_id}는 현재 사용중입니다.")
return
+ # Check if charger is unavailable
+ if self.charger_status_vars[charger_id - 1].get() == "Unavailable":
+ messagebox.showinfo("충전기 사용 불가", f"충전기 {charger_id}는 현재 사용할 수 없습니다.")
+ return
+
# First show login window
LoginWindow(self, charger_id, lambda: self.on_login_success(charger_id),
self.ocpp_client, self.event_loop)
diff --git a/gui_client.py b/gui_client.py
index 9d91ec4..e716794 100644
--- a/gui_client.py
+++ b/gui_client.py
@@ -35,6 +35,7 @@ def __init__(self, app, websocket_url: str, serial_port: str = None, baud_rate:
self.seq_num_counter = [1] * NUM_EVSE # 시퀀스 넘버 카운터
self.boot_notification_sent = False
self.server_tx_id_received = False # 서버에서 트랜잭션 ID를 받았는지 여부
+ self.tx_id_lock = asyncio.Lock() # 트랜잭션 ID 생성을 위한 락 추가
self.load3_mv = [0.0] * 10
self.running = False
@@ -46,6 +47,18 @@ def __init__(self, app, websocket_url: str, serial_port: str = None, baud_rate:
# 트랜잭션 시작 상태 추적을 위한 변수 추가
self.transaction_started = [False] * NUM_EVSE
+
+ # 충전 대기 상태 플래그 추가
+ self.charging_pending = [False] * NUM_EVSE # 충전 대기 상태 플래그
+
+ # 충전기 가용성 상태 추적 변수 추가
+ self.charger_available = [True] * NUM_EVSE # 초기값은, 모든 충전기가 사용 가능
+
+ # ChangeAvailability 콜백 등록
+ self.comm.set_change_availability_callback(self.handle_change_availability)
+
+ # RequestStopTransaction 콜백 등록
+ self.comm.set_stop_transaction_callback(self.handle_request_stop_transaction)
def is_raspberry_pi(self):
"""라즈베리파이 환경인지 확인"""
@@ -126,28 +139,30 @@ async def send_transaction_event_started(self, evse_id: int) -> bool:
self.app.log(f"EVSE {evse_id}: 이미 트랜잭션이 시작되었습니다. 중복 이벤트 무시.")
return True
- # 트랜잭션 ID 관리
- # 서버에서 트랜잭션 ID를 받은 적이 없고, 서버에서 받은 트랜잭션 ID가 있으면 사용
- if not self.server_tx_id_received and hasattr(self.comm, 'last_transaction_id') and self.comm.last_transaction_id:
- try:
- # 'tx-001' 형태에서 숫자 부분만 추출
- tx_id = self.comm.last_transaction_id
- if tx_id.startswith('tx-'):
- tx_num = int(tx_id[3:]) # 'tx-001'에서 '001'을 추출하여 정수로 변환
- # 다음 트랜잭션 ID를 위해 +1
- self.transaction_id_counter = tx_num + 1
- self.app.log(f"서버 응답에서 트랜잭션 ID({tx_id})를 기반으로 다음 ID 설정: tx-{self.transaction_id_counter:03d}")
- self.server_tx_id_received = True # 서버에서 ID를 받았음을 표시
- except (ValueError, AttributeError) as e:
- self.app.log(f"트랜잭션 ID 파싱 오류: {e}. 기본 카운터 사용.")
-
- # 현재 충전기에 트랜잭션 ID 할당
- current_tx_id = self.transaction_id_counter
- self.transaction_ids[evse_id - 1] = current_tx_id
- self.app.log(f"충전기 {evse_id}에 트랜잭션 ID tx-{current_tx_id:03d} 할당")
-
- # 다음 트랜잭션을 위해 카운터 증가 (다음 충전기가 사용할 ID 준비)
- self.transaction_id_counter += 1
+ # 트랜잭션 ID 생성 및 할당 (락을 사용하여 동기화)
+ async with self.tx_id_lock:
+ # 트랜잭션 ID 관리
+ # 서버에서 트랜잭션 ID를 받은 적이 없고, 서버에서 받은 트랜잭션 ID가 있으면 사용
+ if not self.server_tx_id_received and hasattr(self.comm, 'last_transaction_id') and self.comm.last_transaction_id:
+ try:
+ # 'tx-001' 형태에서 숫자 부분만 추출
+ tx_id = self.comm.last_transaction_id
+ if tx_id.startswith('tx-'):
+ tx_num = int(tx_id[3:]) # 'tx-001'에서 '001'을 추출하여 정수로 변환
+ # 다음 트랜잭션 ID를 위해 +1
+ self.transaction_id_counter = tx_num + 1
+ self.app.log(f"서버 응답에서 트랜잭션 ID({tx_id})를 기반으로 다음 ID 설정: tx-{self.transaction_id_counter:03d}")
+ self.server_tx_id_received = True # 서버에서 ID를 받았음을 표시
+ except (ValueError, AttributeError) as e:
+ self.app.log(f"트랜잭션 ID 파싱 오류: {e}. 기본 카운터 사용.")
+
+ # 현재 충전기에 트랜잭션 ID 할당
+ current_tx_id = self.transaction_id_counter
+ self.transaction_ids[evse_id - 1] = current_tx_id
+ self.app.log(f"충전기 {evse_id}에 트랜잭션 ID tx-{current_tx_id:03d} 할당")
+
+ # 다음 트랜잭션을 위해 카운터 증가 (다음 충전기가 사용할 ID 준비)
+ self.transaction_id_counter += 1
# TransactionEvent 메시지 생성
message = {
@@ -269,11 +284,40 @@ async def send_transaction_event_ended(self, evse_id: int, power_value: int) ->
}
}
self.seq_num_counter[evse_id - 1] += 1
+
+ # 응답 수신을 위해 미리 total_price를 초기화
+ self.comm.total_price = None
+
+ # 메시지 전송
success = await self.comm.send_message(message)
if success:
self.app.log(f"EVSE {evse_id}: 충전 종료 이벤트 전송됨, 마지막 보고된 전력 [{power_value}W] (트랜잭션 ID: tx-{self.transaction_ids[evse_id - 1]:03d})")
- self.transaction_started[evse_id - 1] = False # 트랜잭션 종료 상태로 변경
- self.transaction_ids[evse_id - 1] = None # 트랜잭션 ID 초기화
+
+ # 서버 응답을 기다림 (최대 3초)
+ wait_time = 0
+ max_wait = 30 # 100ms * 30 = 3초
+ while wait_time < max_wait and self.comm.total_price is None:
+ await asyncio.sleep(0.1)
+ wait_time += 1
+
+ # total_price가 설정된 경우에만 UI 업데이트
+ if self.comm.total_price is not None:
+ total_price = self.comm.total_price
+ self.app.log(f"EVSE {evse_id}: 총 충전 금액: {total_price}원")
+
+ # GUI에 총 금액 표시 업데이트
+ if hasattr(self.app, 'update_total_price'):
+ self.app.update_total_price(evse_id, total_price)
+
+ # 사용 후 초기화
+ self.comm.total_price = None
+ else:
+ self.app.log(f"EVSE {evse_id}: 서버에서 총 금액 정보를 받지 못했습니다.")
+
+ # 트랜잭션 상태 초기화
+ self.transaction_started[evse_id - 1] = False
+ self.transaction_ids[evse_id - 1] = None
+
return success
async def send_meter_values(self, evse_id: int, power_value: int) -> bool:
@@ -343,6 +387,7 @@ def get_load3_data(self, number_of_load: int) -> bool:
self.serial_data_valid = False
return False
values = data.strip().split()
+ self.app.log(f"수신된 데이터: {values}")
for i in range(min(len(values), 10)):
try:
self.load3_mv[i] = float(values[i])
@@ -419,13 +464,20 @@ def update_power_data(self, evse_id: int, power_value: int):
async def check_charging_start(self):
"""충전 시작 확인"""
for i in range(NUM_EVSE):
- if self.prev_power_data[i] == 0 and self.power_data[i] > 0:
- evse_id = i + 1
- # 상태 알림만 보내고, 트랜잭션 이벤트는 start_charging에서만 보냄
+ evse_id = i + 1
+
+ # 충전 대기 상태이고 전력값이 일정 이상이면 트랜잭션 시작
+ if self.charging_pending[i] and self.power_data[i] > 100: # 100W 이상일 때
+ self.charging_pending[i] = False # 대기 상태 해제
+
+ # 이제 실제 측정값으로 트랜잭션 시작 이벤트 전송
+ await self.send_transaction_event_started(evse_id)
+ self.app.log(f"충전기 {evse_id}: 실제 전력 감지됨, 트랜잭션 시작 ({self.power_data[i]}W)")
+
+ # 기존 로직
+ elif self.prev_power_data[i] == 0 and self.power_data[i] > 0:
if not self.transaction_started[i]:
await self.send_status_notification(evse_id, ConnectorStatus.OCCUPIED)
- # 트랜잭션 이벤트는 start_charging에서 이미 보냈으므로 여기서는 보내지 않음
- # await self.send_transaction_event_started(evse_id)
async def report_power_usage(self):
"""전력 사용량 보고"""
@@ -450,25 +502,67 @@ async def check_charging_end(self):
self.charging_active[i] = False
self.prev_power_data[i] = self.power_data[i]
+ async def handle_change_availability(self, evse_id: int, is_operative: bool) -> bool:
+ """서버로부터 ChangeAvailability 요청 처리"""
+ try:
+ if 1 <= evse_id <= NUM_EVSE:
+ port_idx = evse_id - 1
+
+ # 충전기 가용성 상태 업데이트
+ self.charger_available[port_idx] = is_operative
+
+ # UI 업데이트
+ if is_operative:
+ # Available 상태로 변경 (사용 가능)
+ await self.send_status_notification(evse_id, ConnectorStatus.AVAILABLE)
+ self.app.log(f"충전기 {evse_id}: 서버 요청에 의해 '사용 가능' 상태로 변경되었습니다.")
+ else:
+ # Unavailable 상태로 변경 (사용 불가)
+ await self.send_status_notification(evse_id, ConnectorStatus.UNAVAILABLE)
+ self.app.log(f"충전기 {evse_id}: 서버 요청에 의해 '사용 불가' 상태로 변경되었습니다.")
+
+ # 충전 중이면 충전 중지
+ if self.charging_active[port_idx]:
+ await self.stop_charging(evse_id)
+
+ return True
+ else:
+ self.app.log(f"유효하지 않은 충전기 ID: {evse_id}")
+ return False
+ except Exception as e:
+ self.app.log(f"ChangeAvailability 처리 중 오류: {e}")
+ return False
+
async def start_charging(self, evse_id: int, power_value: int):
"""충전 시작"""
if 1 <= evse_id <= NUM_EVSE:
port_idx = evse_id - 1
+
+ # 충전기가 사용 불가 상태인 경우 충전 불가
+ if not self.charger_available[port_idx]:
+ self.app.log(f"충전기 {evse_id}는 현재 사용 불가 상태입니다.")
+ return False
+
self.manual_power[port_idx] = power_value
self.charging_active[port_idx] = True
self.app.log(f"충전기 {evse_id}의 충전을 시작합니다. 전력: {power_value}W")
-
+
# 시리얼 연결이 있는 경우 전력 공급 명령 전송
if self.use_serial:
self.send_power_control_command(evse_id, True)
- # Update status to Occupied
- await self.send_status_notification(evse_id, ConnectorStatus.OCCUPIED)
-
- # 트랜잭션 시작 이벤트 전송 (여기서만 전송)
- await self.send_transaction_event_started(evse_id)
+ # 상태만 Occupied로 변경하고, 트랜잭션 이벤트는 아직 보내지 않음
+ await self.send_status_notification(evse_id, ConnectorStatus.OCCUPIED)
- return True
+ # 트랜잭션 시작 플래그 설정 (아직 이벤트는 보내지 않음)
+ # 수정: 특정 충전기만 대기 상태로 설정
+ self.charging_pending[evse_id - 1] = True # 해당 충전기만 대기 상태로 설정
+ return True
+ else:
+ # 시리얼 연결이 없는 경우 기존처럼 처리
+ await self.send_status_notification(evse_id, ConnectorStatus.OCCUPIED)
+ await self.send_transaction_event_started(evse_id)
+ return True
return False
async def stop_charging(self, evse_id: int):
@@ -491,6 +585,29 @@ async def stop_charging(self, evse_id: int):
return True
return False
+ async def handle_request_stop_transaction(self, evse_id: int) -> bool:
+ """서버로부터 RequestStopTransaction 요청 처리"""
+ try:
+ if 1 <= evse_id <= NUM_EVSE:
+ port_idx = evse_id - 1
+
+ # 충전 중인지 확인
+ if self.charging_active[port_idx]:
+ self.app.log(f"충전기 {evse_id}: 서버 요청에 의해 충전이 중지됩니다.")
+
+ # 충전 중지 호출
+ success = await self.stop_charging(evse_id)
+ return success
+ else:
+ self.app.log(f"충전기 {evse_id}: 충전 중이 아니므로 중지 요청이 거부되었습니다.")
+ return False
+ else:
+ self.app.log(f"유효하지 않은 충전기 ID: {evse_id}")
+ return False
+ except Exception as e:
+ self.app.log(f"RequestStopTransaction 처리 중 오류: {e}")
+ return False
+
async def run_loop(self):
"""메인 루프 실행"""
self.running = True
@@ -514,6 +631,7 @@ async def run_loop(self):
self.power_data[i] = 0
self.prev_power_data[i] = 0
self.transaction_started[i] = False # 트랜잭션 시작 상태 초기화
+ self.manual_power[i] = 0 # 초기 수동 전력값을 0으로 설정
if websocket_connected:
await self.send_boot_notification()
@@ -549,10 +667,16 @@ async def run_loop(self):
# Generate temporary data for active chargers
for i in range(NUM_EVSE):
if self.charging_active[i]:
- base_power = self.manual_power[i] if self.manual_power[i] > 0 else 3000
- variation = random.uniform(-200, 200)
+ base_power = self.manual_power[i]
+ # 전력값이 0이면 변동 없이 유지, 0보다 크면 변동 추가
+ if base_power > 0:
+ variation = random.uniform(-200, 200)
+ power_with_variation = max(0, base_power + variation)
+ else:
+ power_with_variation = 0
+
self.load3_mv[i*2] = 220.0 # Voltage
- self.load3_mv[i*2+1] = (base_power + variation) / 220.0 # Current
+ self.load3_mv[i*2+1] = power_with_variation / 220.0 if power_with_variation > 0 else 0.0 # Current
else:
self.load3_mv[i*2] = 0.0
self.load3_mv[i*2+1] = 0.0
@@ -571,7 +695,7 @@ async def run_loop(self):
else:
self.app.log("데이터 읽기 오류")
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.5) # 0.1초에서 0.5초로 변경
except Exception as e:
self.app.log(f"오류 발생: {e}")
finally:
diff --git a/ocpp_comm.py b/ocpp_comm.py
index 32c4d5b..0be5ae2 100644
--- a/ocpp_comm.py
+++ b/ocpp_comm.py
@@ -35,6 +35,9 @@ def __init__(self, websocket_url, serial_port=None, baud_rate=2400, max_retries=
# 트랜잭션 ID 정보 저장
self.last_transaction_id = None # 마지막으로 수신한 트랜잭션 ID
+
+ # 충전 완료 후 총 금액 정보 저장
+ self.total_price = None # 트랜잭션 종료 시 받은 총 금액
async def connect_websocket(self) -> bool:
"""WebSocket 연결"""
@@ -138,6 +141,11 @@ async def _send_message_and_wait_response(self, message: dict) -> bool:
retry_info = f" (재시도: {message.get('retry_count', 0)}/{self.max_retries})" if message.get('retry_count', 0) > 0 else ""
print(f"[WebSocket sending]{retry_info} {json_message}")
+ # 트랜잭션 종료 이벤트인지 확인
+ is_tx_ended = message.get("action") == "TransactionEvent" and message.get("payload", {}).get("eventType") == "Ended"
+ if is_tx_ended:
+ print("트랜잭션 종료 이벤트 전송 - 응답에서 총 금액 정보 확인 예정")
+
await self.websocket.send(json_message)
# 응답 수신 태스크 시작
@@ -174,13 +182,34 @@ async def receive_message(self):
response = await self.websocket.recv()
print(f"서버 응답: {response}")
- # 응답 저장 및 이벤트 설정
- self.last_response = response
- self.response_event.set()
-
# 응답 파싱
try:
response_data = json.loads(response)
+
+ # 요청 메시지 처리 (CALL - messageTypeId = 2)
+ if len(response_data) >= 4 and response_data[0] == 2:
+ message_id = response_data[1]
+ action = response_data[2]
+ payload = response_data[3]
+
+ # 요청 처리 완료 후 응답 이벤트 설정 (중요: 응답 대기 타임아웃 방지)
+ self.last_response = "request_handled"
+ self.response_event.set()
+
+ # ChangeAvailability 요청 처리
+ if action == "ChangeAvailability":
+ await self.handle_change_availability(message_id, payload)
+ return
+
+ # RequestStopTransaction 요청 처리 추가
+ if action == "RequestStopTransaction":
+ await self.handle_request_stop_transaction(message_id, payload)
+ return
+
+ # 일반 응답 처리 (기존 로직)
+ self.last_response = response
+ self.response_event.set()
+
if len(response_data) >= 3 and response_data[0] == 3: # 응답 메시지인 경우
payload = response_data[2] # 응답 페이로드
message_id = response_data[1] # 메시지 ID
@@ -198,6 +227,17 @@ async def receive_message(self):
tx_id = f"tx-{int(tx_id):03d}"
self.last_transaction_id = tx_id
print(f"트랜잭션 ID 업데이트: {self.last_transaction_id}")
+
+ # 총 금액 정보 추출 (TransactionEvent.Ended 응답에 포함)
+ if "totalPrice" in payload:
+ price_value = payload["totalPrice"]
+ # 숫자인지 확인하고 유효한 경우에만 설정
+ if isinstance(price_value, (int, float)) and price_value >= 0:
+ # total_price 설정 (이 값은 send_transaction_event_ended에서 확인됨)
+ self.total_price = price_value
+ print(f"총 금액 정보 수신: {self.total_price}원")
+ else:
+ print(f"유효하지 않은 금액 정보: {price_value}")
except Exception as e:
print(f"응답 파싱 중 오류: {e}")
@@ -208,3 +248,91 @@ async def receive_message(self):
self.websocket = None
# 오류 발생 시에도 이벤트 설정 (대기 중인 태스크가 진행되도록)
self.response_event.set()
+
+ async def handle_change_availability(self, message_id, payload):
+ """ChangeAvailability 요청 처리"""
+ try:
+ print(f"ChangeAvailability 요청 수신: {payload}")
+
+ # 요청 파라미터 확인
+ operational_status = payload.get("operationalStatus")
+ evse_id = payload.get("evse", {}).get("id")
+
+ if not operational_status or not evse_id:
+ print("필수 파라미터 누락")
+ # 오류가 있어도 항상 일반 응답 전송
+ await self.send_availability_response(message_id, False)
+ return
+
+ # 이벤트 발생 (GUI 클라이언트에서 처리)
+ if hasattr(self, 'change_availability_callback') and callable(self.change_availability_callback):
+ is_operative = (operational_status == "Operative")
+ success = await self.change_availability_callback(evse_id, is_operative)
+
+ # 응답 전송
+ await self.send_availability_response(message_id, success)
+ else:
+ print("change_availability_callback이 설정되지 않음")
+ await self.send_availability_response(message_id, False)
+
+ except Exception as e:
+ print(f"ChangeAvailability 처리 중 오류: {e}")
+ # 오류가 발생해도 항상 일반 응답으로 처리
+ await self.send_availability_response(message_id, False)
+
+ async def handle_request_stop_transaction(self, message_id, payload):
+ """RequestStopTransaction 요청 처리"""
+ try:
+ print(f"RequestStopTransaction 요청 수신: {payload}")
+
+ # 요청 파라미터 확인
+ evse_id = payload.get("evseId")
+
+ if not evse_id:
+ print("필수 파라미터 누락")
+ await self.send_stop_transaction_response(message_id, False)
+ return
+
+ # 문자열이면 정수로 변환
+ try:
+ evse_id = int(evse_id)
+ except ValueError:
+ print(f"유효하지 않은 충전기 ID: {evse_id}")
+ await self.send_stop_transaction_response(message_id, False)
+ return
+
+ # 콜백 호출
+ if hasattr(self, 'stop_transaction_callback') and callable(self.stop_transaction_callback):
+ success = await self.stop_transaction_callback(evse_id)
+ await self.send_stop_transaction_response(message_id, success)
+ else:
+ print("stop_transaction_callback이 설정되지 않음")
+ await self.send_stop_transaction_response(message_id, False)
+
+ except Exception as e:
+ print(f"RequestStopTransaction 처리 중 오류: {e}")
+ await self.send_stop_transaction_response(message_id, False)
+
+ async def send_stop_transaction_response(self, message_id, success):
+ """RequestStopTransaction 응답 전송"""
+ status = "Accepted" if success else "Rejected"
+ response = json.dumps([3, message_id, {"status": status}])
+
+ print(f"RequestStopTransaction 응답 전송: {response}")
+ await self.websocket.send(response)
+
+ def set_stop_transaction_callback(self, callback):
+ """RequestStopTransaction 콜백 설정"""
+ self.stop_transaction_callback = callback
+
+ async def send_availability_response(self, message_id, success):
+ """ChangeAvailability 응답 전송"""
+ status = "Accepted" if success else "Rejected"
+ response = json.dumps([3, message_id, {"status": status}])
+
+ print(f"ChangeAvailability 응답 전송: {response}")
+ await self.websocket.send(response)
+
+ def set_change_availability_callback(self, callback):
+ """ChangeAvailability 콜백 설정"""
+ self.change_availability_callback = callback
diff --git a/tempCodeRunnerFile.py b/tempCodeRunnerFile.py
new file mode 100644
index 0000000..a604cb7
--- /dev/null
+++ b/tempCodeRunnerFile.py
@@ -0,0 +1 @@
+OcppGuiApp
\ No newline at end of file
diff --git a/visual_dashboard.py b/visual_dashboard.py
index af0b70a..1ac4e5a 100644
--- a/visual_dashboard.py
+++ b/visual_dashboard.py
@@ -220,6 +220,17 @@ def create_widgets(self):
power_value = ttk.Label(power_frame, textvariable=self.power_var)
power_value.pack(side=tk.LEFT)
+ # Total price info
+ price_frame = ttk.Frame(info_frame)
+ price_frame.pack(fill=tk.X, pady=2)
+
+ price_label = ttk.Label(price_frame, text="총 금액:", width=10)
+ price_label.pack(side=tk.LEFT)
+
+ self.total_price_var = tk.StringVar(value="-")
+ price_value = ttk.Label(price_frame, textvariable=self.total_price_var, font=("Arial", 10, "bold"))
+ price_value.pack(side=tk.LEFT)
+
# Last updated info
update_frame = ttk.Frame(info_frame)
update_frame.pack(fill=tk.X, pady=2)
@@ -251,3 +262,17 @@ def update_power(self, power):
self.power_var.set(f"{power} W")
self.power_meter.update_power(power)
self.update_var.set(time.strftime('%H:%M:%S'))
+
+ def update_total_price(self, total_price):
+ """총 금액 정보 업데이트"""
+ if total_price is not None:
+ self.total_price_var.set(f"{total_price}원")
+
+ # 상태가 Available이 아니면 사용 가능으로 변경 (충전 완료 시)
+ if self.status_var.get() != "사용 가능":
+ self.update_status("Available")
+
+ # 현재 시간 업데이트
+ self.update_var.set(time.strftime('%H:%M:%S'))
+ else:
+ self.total_price_var.set("-")