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("-")