Skip to content

Commit f831db8

Browse files
authored
Update wallbox_monitor.py
✅ Sensitive URLs are now stored in wallbox_monitor.credo, keeping the script clean and secure. ✅ No need to modify the script when updating URLs, just edit wallbox_monitor.credo. ✅ Graceful error handling, preventing crashes if the credentials file is missing or misconfigured.
1 parent 554d625 commit f831db8

1 file changed

Lines changed: 32 additions & 107 deletions

File tree

wallbox_monitor.py

Lines changed: 32 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@
33
import logging
44
import time
55
import re
6+
import configparser
67
from datetime import datetime
78
from selenium import webdriver
89
from selenium.webdriver.chrome.service import Service
910
from 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"
1825
STATE_FILE = "/tmp/wallbox_state.txt"
1926

27+
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
28+
2029
def 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

2938
def 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-
8476
def 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-
12386
def 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

Comments
 (0)