88from selenium import webdriver
99from selenium .webdriver .chrome .service import Service
1010from selenium .webdriver .common .by import By
11+ from selenium .webdriver .support .ui import WebDriverWait
12+ from selenium .webdriver .support import expected_conditions as EC
13+
14+ LOG_FILE = "/home/pi/wallbox_monitor.log"
15+ logging .basicConfig (filename = LOG_FILE , level = logging .INFO , format = "%(asctime)s - %(levelname)s - %(message)s" )
1116
1217# Load configuration from credentials file
1318CONFIG_FILE = "wallbox_monitor.credo"
2126 logging .error (f"Configuration error: { e } " )
2227 raise SystemExit ("Error loading credentials. Check 'wallbox_monitor.credo'." )
2328
24- LOG_FILE = "/home/pi/wallbox_monitor.log"
2529STATE_FILE = "/tmp/wallbox_state.txt"
2630
27- logging .basicConfig (filename = LOG_FILE , level = logging .INFO , format = "%(asctime)s - %(levelname)s - %(message)s" )
28-
2931def get_browser ():
3032 """Starts a headless browser session using Chromium."""
3133 options = webdriver .ChromeOptions ()
@@ -36,43 +38,60 @@ def get_browser():
3638 return webdriver .Chrome (service = service , options = options )
3739
3840def fetch_charging_status (driver ):
39- """Fetch charging rate and consumed energy from the Wallbox ."""
41+ """Uses Selenium to get the dynamically updated charging rate and consumed energy."""
4042 try :
4143 driver .get (WALLBOX_URL )
4244
4345 charging_rate = None
4446 consumed_energy_wh = None
4547
46- for _ in range (30 ): # Retry for up to 30 seconds
48+ # Wait up to 30 seconds for both values to be populated
49+ for _ in range (30 ): # Retry every second for up to 30 seconds
4750 try :
48- charging_text = driver .find_element (By .ID , "chargingRate" ).get_attribute ("value" ).strip ()
51+ # Get charging rate
52+ charging_element = driver .find_element (By .ID , "chargingRate" )
53+ charging_text = charging_element .get_attribute ("value" ).strip ()
4954 if charging_text :
50- match = re .search (r"([\d.]+)\s*kw" , charging_text )
51- if match :
52- charging_rate = float (match .group (1 ))
55+ match_charging = re .search (r"([\d.]+)\s*kw" , charging_text )
56+ if match_charging :
57+ charging_rate = float (match_charging .group (1 ))
5358
54- consumed_text = driver .find_element (By .ID , "consumed" ).get_attribute ("value" ).strip ()
59+ # Get consumed energy
60+ consumed_element = driver .find_element (By .ID , "consumed" )
61+ consumed_text = consumed_element .get_attribute ("value" ).strip ()
5562 if consumed_text :
56- match = re .search (r"([\d.]+)\s*(wh|kWh)" , consumed_text )
57- if match :
58- consumed_energy_wh = float (match .group (1 ))
63+ match_consumed = re .search (r"([\d.]+)\s*(wh|kWh)" , consumed_text )
64+ if match_consumed :
65+ consumed_energy_wh = float (match_consumed .group (1 ))
5966 if "kWh" in consumed_text :
6067 consumed_energy_wh *= 1000 # Convert kWh to Wh
6168
69+ # Exit loop early if both values are found
6270 if charging_rate is not None and consumed_energy_wh is not None :
6371 break
6472
6573 except Exception :
66- pass # Retry
74+ pass # Ignore errors and retry
6775
68- time .sleep (1 )
76+ time .sleep (1 ) # Wait 1 second before retrying
6977
7078 return charging_rate , consumed_energy_wh
7179
7280 except Exception as e :
7381 logging .error (f"Error fetching charging status: { e } " )
7482 return None , None
7583
84+
85+ def format_energy (wh ):
86+ """Converts Wh to kWh if necessary and formats output with two decimal places."""
87+ if wh is None :
88+ return "0.00 kWh"
89+ return f"{ wh / 1000 :.2f} kWh" if wh >= 1000 else f"{ wh :.2f} Wh"
90+
91+ def german_timestamp ():
92+ """Returns the current time in German short format: DD.MM.YY, HH:MM."""
93+ return datetime .now ().strftime ("%d.%m.%y, %H:%M" )
94+
7695def send_discord_notification (message ):
7796 """Sends a notification to Discord."""
7897 import requests
@@ -83,16 +102,79 @@ def send_discord_notification(message):
83102 except requests .RequestException as e :
84103 logging .error (f"Error sending Discord notification: { e } " )
85104
105+ def get_last_state ():
106+ """Reads the last state and charging start time from the state file."""
107+ try :
108+ with open (STATE_FILE , "r" ) as f :
109+ data = f .read ().strip ()
110+ if data .startswith ("charging:" ):
111+ parts = data .split (":" )
112+ try :
113+ timestamp = float (parts [1 ]) if parts [1 ] != "None" else 0.0
114+ power = float (parts [2 ]) if parts [2 ] != "None" else 0.0
115+ return "charging" , timestamp , power
116+ except ValueError :
117+ return "idle" , None , None # Reset to idle if parsing fails
118+ return data , None , None # "idle"
119+ except FileNotFoundError :
120+ return "idle" , None , None
121+
122+ def save_last_state (state , charging_power = 0.0 ):
123+ """Saves the current state and timestamp if charging starts."""
124+ with open (STATE_FILE , "w" ) as f :
125+ if state == "charging" :
126+ charging_power = charging_power if charging_power is not None else 0.0 # Ensure valid number
127+ f .write (f"charging:{ time .time ()} :{ charging_power :.2f} " ) # Store timestamp + power
128+ else :
129+ f .write ("idle" ) # Reset if charging stops
130+
86131def main ():
87- """Main execution function to monitor the Wallbox status ."""
132+ """Checks wallbox status and triggers notifications when state changes ."""
88133 driver = get_browser ()
134+
89135 try :
90- charging_rate , _ = fetch_charging_status (driver )
136+ charging_rate , consumed_energy_wh = fetch_charging_status (driver )
137+
138+ print (f"🔍 Charging Rate: { charging_rate } , Consumed Energy: { consumed_energy_wh } " )
91139
92140 if charging_rate is not None :
93- message = f"⚡ Charging Rate: { charging_rate :.2f} kW"
94- print (f"📢 Sending Discord Notification: { message } " )
95- send_discord_notification (message )
141+ new_state = "charging" if charging_rate >= 1.0 else "idle"
142+ last_state , start_time , stored_power = get_last_state ()
143+
144+ timestamp = german_timestamp ()
145+
146+ print (f"🔄 Last State: { last_state } , New State: { new_state } " )
147+
148+ if last_state != new_state : # Only trigger on state change
149+ if new_state == "charging" :
150+ message = f"⚡ { timestamp } : charging started."
151+ print (f"📢 Sending Discord Notification: { message } " )
152+ send_discord_notification (message )
153+ save_last_state (new_state , charging_rate ) # Store start time & power
154+ else :
155+ message = f"🔋 { timestamp } : charging stopped."
156+ print (f"📢 Sending Discord Notification: { message } " )
157+ send_discord_notification (message )
158+ save_last_state (new_state ) # Reset state
159+
160+ # If charging, check if 5 minutes have passed
161+ elif new_state == "charging" and start_time is not None :
162+ elapsed_time = time .time () - start_time
163+ print (f"⏳ Elapsed Charging Time: { elapsed_time :.2f} seconds" )
164+
165+ if elapsed_time >= 300 : # 300 seconds = 5 minutes
166+ latest_charging_rate , _ = fetch_charging_status (driver ) # Fetch latest power
167+ message = f"⚡ charging power: { latest_charging_rate :.2f} kW"
168+ print (f"📢 Sending Discord Notification: { message } " )
169+ send_discord_notification (message )
170+ save_last_state ("charging" ) # Prevent duplicate notifications
171+
172+ # If charging stopped, send a separate consumption message
173+ if last_state == "charging" and new_state == "idle" and consumed_energy_wh is not None :
174+ formatted_energy = format_energy (consumed_energy_wh )
175+ message = f"⚡ consumed energy: { formatted_energy } "
176+ print (f"📢 Sending Discord Notification: { message } " )
177+ send_discord_notification (message )
96178
97179 finally :
98180 driver .quit ()
0 commit comments