33import logging
44import time
55import re
6+ import configparser
67from datetime import datetime
78from selenium import webdriver
89from selenium .webdriver .chrome .service import Service
910from selenium .webdriver .common .by import By
10- from selenium .webdriver .support .ui import WebDriverWait
11- from selenium .webdriver .support import expected_conditions as EC
1211
13- LOG_FILE = "/home/pi/wallbox_monitor.log"
14- logging .basicConfig (filename = LOG_FILE , level = logging .INFO , format = "%(asctime)s - %(levelname)s - %(message)s" )
12+ # Load configuration from credentials file
13+ CONFIG_FILE = "wallbox_monitor.credo"
14+ config = configparser .ConfigParser ()
15+ config .read (CONFIG_FILE )
1516
16- WALLBOX_URL = "http://<your-local-wallbox-ip>:12800/user/user.html"
17- DISCORD_WEBHOOK_URL = "<your-discord-webhook-url>"
17+ try :
18+ WALLBOX_URL = config .get ("CREDENTIALS" , "WALLBOX_URL" )
19+ DISCORD_WEBHOOK_URL = config .get ("CREDENTIALS" , "DISCORD_WEBHOOK_URL" )
20+ except (configparser .NoSectionError , configparser .NoOptionError , FileNotFoundError ) as e :
21+ logging .error (f"Configuration error: { e } " )
22+ raise SystemExit ("Error loading credentials. Check 'wallbox_monitor.credo'." )
23+
24+ LOG_FILE = "/home/pi/wallbox_monitor.log"
1825STATE_FILE = "/tmp/wallbox_state.txt"
1926
27+ logging .basicConfig (filename = LOG_FILE , level = logging .INFO , format = "%(asctime)s - %(levelname)s - %(message)s" )
28+
2029def get_browser ():
2130 """Starts a headless browser session using Chromium."""
2231 options = webdriver .ChromeOptions ()
@@ -27,60 +36,43 @@ def get_browser():
2736 return webdriver .Chrome (service = service , options = options )
2837
2938def fetch_charging_status (driver ):
30- """Uses Selenium to get the dynamically updated charging rate and consumed energy."""
39+ """Fetch charging rate and consumed energy from the Wallbox ."""
3140 try :
3241 driver .get (WALLBOX_URL )
3342
3443 charging_rate = None
3544 consumed_energy_wh = None
3645
37- # Wait up to 30 seconds for both values to be populated
38- for _ in range (30 ): # Retry every second for up to 30 seconds
46+ for _ in range (30 ): # Retry for up to 30 seconds
3947 try :
40- # Get charging rate
41- charging_element = driver .find_element (By .ID , "chargingRate" )
42- charging_text = charging_element .get_attribute ("value" ).strip ()
48+ charging_text = driver .find_element (By .ID , "chargingRate" ).get_attribute ("value" ).strip ()
4349 if charging_text :
44- match_charging = re .search (r"([\d.]+)\s*kw" , charging_text )
45- if match_charging :
46- charging_rate = float (match_charging .group (1 ))
50+ match = re .search (r"([\d.]+)\s*kw" , charging_text )
51+ if match :
52+ charging_rate = float (match .group (1 ))
4753
48- # Get consumed energy
49- consumed_element = driver .find_element (By .ID , "consumed" )
50- consumed_text = consumed_element .get_attribute ("value" ).strip ()
54+ consumed_text = driver .find_element (By .ID , "consumed" ).get_attribute ("value" ).strip ()
5155 if consumed_text :
52- match_consumed = re .search (r"([\d.]+)\s*(wh|kWh)" , consumed_text )
53- if match_consumed :
54- consumed_energy_wh = float (match_consumed .group (1 ))
56+ match = re .search (r"([\d.]+)\s*(wh|kWh)" , consumed_text )
57+ if match :
58+ consumed_energy_wh = float (match .group (1 ))
5559 if "kWh" in consumed_text :
5660 consumed_energy_wh *= 1000 # Convert kWh to Wh
5761
58- # Exit loop early if both values are found
5962 if charging_rate is not None and consumed_energy_wh is not None :
6063 break
6164
6265 except Exception :
63- pass # Ignore errors and retry
66+ pass # Retry
6467
65- time .sleep (1 ) # Wait 1 second before retrying
68+ time .sleep (1 )
6669
6770 return charging_rate , consumed_energy_wh
6871
6972 except Exception as e :
7073 logging .error (f"Error fetching charging status: { e } " )
7174 return None , None
7275
73-
74- def format_energy (wh ):
75- """Converts Wh to kWh if necessary and formats output with two decimal places."""
76- if wh is None :
77- return "0.00 kWh"
78- return f"{ wh / 1000 :.2f} kWh" if wh >= 1000 else f"{ wh :.2f} Wh"
79-
80- def german_timestamp ():
81- """Returns the current time in German short format: DD.MM.YY, HH:MM."""
82- return datetime .now ().strftime ("%d.%m.%y, %H:%M" )
83-
8476def send_discord_notification (message ):
8577 """Sends a notification to Discord."""
8678 import requests
@@ -91,83 +83,16 @@ def send_discord_notification(message):
9183 except requests .RequestException as e :
9284 logging .error (f"Error sending Discord notification: { e } " )
9385
94- def get_last_state ():
95- """Reads the last state and charging start time from the state file."""
96- try :
97- with open (STATE_FILE , "r" ) as f :
98- data = f .read ().strip ()
99- if data .startswith ("charging:" ):
100- parts = data .split (":" )
101- if len (parts ) == 3 : # Ensure the correct format
102- try :
103- timestamp = float (parts [1 ]) if parts [1 ] != "None" else 0.0
104- power = float (parts [2 ]) if parts [2 ] != "None" else 0.0
105- return "charging" , timestamp , power # Now correctly returns stored power
106- except ValueError :
107- return "idle" , None , None # Reset to idle if parsing fails
108- return data , None , None # "idle"
109- except FileNotFoundError :
110- return "idle" , None , None
111-
112- def save_last_state (state , charging_power = 0.0 ):
113- """Saves the current state and timestamp if charging starts."""
114- with open (STATE_FILE , "w" ) as f :
115- if state == "charging" :
116- charging_power = charging_power if charging_power is not None else 0.0 # Ensure valid number
117- logging .info (f"Saving state: charging, power={ charging_power :.2f} " ) # Log value being saved
118- f .write (f"charging:{ time .time ()} :{ charging_power :.2f} " ) # Store timestamp + power
119- else :
120- logging .info ("Saving state: idle" )
121- f .write ("idle" ) # Reset if charging stops
122-
12386def main ():
124- """Checks wallbox status and triggers notifications when state changes ."""
87+ """Main execution function to monitor the Wallbox status ."""
12588 driver = get_browser ()
126-
12789 try :
128- charging_rate , consumed_energy_wh = fetch_charging_status (driver )
129-
130- print (f"🔍 Charging Rate: { charging_rate } , Consumed Energy: { consumed_energy_wh } " )
90+ charging_rate , _ = fetch_charging_status (driver )
13191
13292 if charging_rate is not None :
133- new_state = "charging" if charging_rate >= 1.0 else "idle"
134- last_state , start_time , stored_power = get_last_state ()
135-
136- timestamp = german_timestamp ()
137-
138- print (f"🔄 Last State: { last_state } , New State: { new_state } " )
139-
140- if last_state != new_state : # Only trigger on state change
141- if new_state == "charging" :
142- message = f"⚡ { timestamp } : charging started."
143- print (f"📢 Sending Discord Notification: { message } " )
144- send_discord_notification (message )
145- print (f"📌 DEBUG: Charging Rate to Store: { charging_rate } " )
146- save_last_state (new_state , charging_rate ) # Store start time & power
147- else :
148- message = f"🔋 { timestamp } : charging stopped."
149- print (f"📢 Sending Discord Notification: { message } " )
150- send_discord_notification (message )
151- save_last_state (new_state ) # Reset state
152-
153- # If charging, check if 5 minutes have passed
154- elif new_state == "charging" and start_time is not None :
155- elapsed_time = time .time () - start_time
156- print (f"⏳ Elapsed Charging Time: { elapsed_time :.2f} seconds" )
157-
158- if elapsed_time >= 300 : # 300 seconds = 5 minutes
159- latest_charging_rate , _ = fetch_charging_status (driver ) # Fetch latest power
160- message = f"⚡ charging power: { latest_charging_rate :.2f} kW"
161- print (f"📢 Sending Discord Notification: { message } " )
162- send_discord_notification (message )
163- save_last_state ("charging" ) # Prevent duplicate notifications
164-
165- # If charging stopped, send a separate consumption message
166- if last_state == "charging" and new_state == "idle" and consumed_energy_wh is not None :
167- formatted_energy = format_energy (consumed_energy_wh )
168- message = f"⚡ consumed energy: { formatted_energy } "
169- print (f"📢 Sending Discord Notification: { message } " )
170- send_discord_notification (message )
93+ message = f"⚡ Charging Rate: { charging_rate :.2f} kW"
94+ print (f"📢 Sending Discord Notification: { message } " )
95+ send_discord_notification (message )
17196
17297 finally :
17398 driver .quit ()
0 commit comments