11#!/usr/bin/env python3
22
3- # v1.2.2
3+ # v1.3
44# wallbox-monitoring - by bjoerrrn
55# github: https://github.com/bjoerrrn/wallbox-monitoring
66# This script is licensed under GNU GPL version 3.0 or above
1313import time
1414import re
1515import configparser
16+ import subprocess
17+ import json
1618from datetime import datetime
1719from selenium import webdriver
1820from selenium .webdriver .chrome .service import Service
@@ -72,7 +74,8 @@ def load_config():
7274 "NTFY_TOPIC" : config .get ("CREDENTIALS" , "NTFY_TOPIC" , fallback = "" ).strip (),
7375 "PUSHOVER_USER_KEY" : config .get ("CREDENTIALS" , "PUSHOVER_USER_KEY" , fallback = "" ).strip (),
7476 "PUSHOVER_API_TOKEN" : config .get ("CREDENTIALS" , "PUSHOVER_API_TOKEN" , fallback = "" ).strip (),
75- "FIXED_PRICE" : float (config .get ("CREDENTIALS" , "FIXED_PRICE" , fallback = "0" )) or 0
77+ "FIXED_PRICE" : float (config .get ("CREDENTIALS" , "FIXED_PRICE" , fallback = "0" )) or 0 ,
78+ "EXTERNAL_LOG_SCRIPT" : config .get ("CREDENTIALS" , "EXTERNAL_LOG_SCRIPT" , fallback = "" ).strip ()
7679 }
7780
7881 debug (f"Loaded configuration: Wallbox URL: { cfg ['WALLBOX_URL' ]} , "
@@ -87,6 +90,7 @@ def load_config():
8790 raise SystemExit (f"Error loading credentials: { e } " )
8891
8992CONFIG = load_config ()
93+ EXTERNAL_LOG_SCRIPT = CONFIG ["EXTERNAL_LOG_SCRIPT" ] # Load globally
9094
9195def get_browser ():
9296 options = webdriver .ChromeOptions ()
@@ -213,7 +217,58 @@ def get_last_state():
213217 "notified" : False ,
214218 "repeat_check" : False ,
215219 }
220+
216221
222+ def external_script (last_state , new_state , start_time , stored_power , notified , total_energy_wh_for_summary , repeat_check ):
223+ if not EXTERNAL_LOG_SCRIPT :
224+ print (f"⚠️ no external log script configured." )
225+ return # No script configured, do nothing
226+
227+ if new_state == "idle" and last_state == "idle" :
228+ return # Do nothing if already idle
229+
230+ if repeat_check :
231+ return # Do nothing if new status is currently being verified
232+
233+ event_data = {
234+ "state" : new_state ,
235+ "start_time" : str (start_time ) if start_time else "0" ,
236+ "stored_power" : stored_power ,
237+ "total_energy_kWh" : (total_energy_wh_for_summary or 0.0 ) / 1000.0 , # Convert Wh to kWh
238+ "notified" : int (notified ),
239+ "repeat_check" : int (repeat_check )
240+ }
241+
242+ try :
243+ subprocess .run ([EXTERNAL_LOG_SCRIPT , json .dumps (event_data )], check = True )
244+ logger .info (f"✅ External script executed successfully: { EXTERNAL_LOG_SCRIPT } " )
245+ except Exception as e :
246+ logger .error (f"⚠️ Error executing external script: { e } " )
247+
248+
249+ def save_last_state (last_state , new_state , stored_power = 0.0 , total_energy_wh_for_summary = 0.0 , notified = False , start_time = None , repeat_check = False ):
250+ with open (STATE_FILE , "w" ) as f :
251+ if new_state in ["charging" , "idle" ]:
252+ start_time_str = str (start_time ) if new_state == "charging" and start_time is not None else "0"
253+ state_str = f"{ new_state } :{ start_time_str } :{ stored_power :.2f} :{ int (notified )} :{ total_energy_wh_for_summary or 0.0 } :{ int (repeat_check )} "
254+ f .write (state_str )
255+ debug (f".. save_last_state(): { state_str } " )
256+ external_script (last_state , new_state , start_time_str , stored_power , notified , total_energy_wh_for_summary , repeat_check )
257+
258+ elif new_state == "disconnected" :
259+ state_str = f"disconnected:{ total_energy_wh_for_summary if total_energy_wh_for_summary is not None else '0.0' } :{ int (notified )} :{ int (repeat_check )} "
260+ f .write (state_str )
261+ debug (f".. save_last_state(): { state_str } " )
262+ external_script (last_state , new_state , None , 0.0 , notified , total_energy_wh_for_summary , repeat_check )
263+
264+ else : # Default to idle
265+ state_str = f"idle:0.0:0.0:{ int (notified )} :{ int (repeat_check )} "
266+ f .write (state_str )
267+ debug (f".. save_last_state(): { state_str } " )
268+ external_script (last_state , new_state , None , 0.0 , notified , 0.0 , repeat_check )
269+
270+
271+ """
217272def save_last_state(state, stored_power=0.0, total_energy_wh_for_summary=0.0, notified=False, start_time=None, repeat_check=False):
218273 with open(STATE_FILE, "w") as f:
219274 if state in ["charging", "idle"]:
@@ -228,8 +283,10 @@ def save_last_state(state, stored_power=0.0, total_energy_wh_for_summary=0.0, no
228283 else:
229284 f.write(f"idle:0.0:0.0:{int(notified)}:{int(repeat_check)}")
230285 debug(f".. save_last_state(): idle:0.0:0.0:{int(notified)}:{int(repeat_check)}")
231-
232- def send_energy_summary (total_energy_wh_for_summary ):
286+ """
287+
288+
289+ def send_energy_summary (last_state , total_energy_wh_for_summary ):
233290 """Sends a summary of total consumed energy when the cable is disconnected."""
234291 if total_energy_wh_for_summary is None or total_energy_wh_for_summary <= 0 :
235292 debug ("Skipping energy summary (no energy recorded)." )
@@ -246,7 +303,7 @@ def send_energy_summary(total_energy_wh_for_summary):
246303 logger .info ("✅ Energy summary sent. Resetting stored energy summary." )
247304
248305 # Reset `total_energy_wh_for_summary` after reporting to avoid reuse
249- save_last_state ("disconnected" , total_energy_wh_for_summary = 0 )
306+ save_last_state (last_state , "disconnected" , total_energy_wh_for_summary = 0 )
250307
251308def fetch_charging_status (driver ):
252309 """Fetches charging rate and total energy from the charger."""
@@ -335,54 +392,55 @@ def main():
335392 if last_state == "charging" and (total_energy_wh is None or charging_rate == 0 ):
336393 if not repeat_check : # First detection, re-run once
337394 debug (f"🔌 { timestamp } : charging interruption detected, verifying..." )
338- save_last_state ("charging" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = notified , start_time = start_time , repeat_check = True )
395+ save_last_state (last_state , "charging" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = notified , start_time = start_time , repeat_check = True )
339396 return # Exit, script will retry next run
340397
341398 # Second run, confirmed interruption
342- send_notification (f"🔌 { timestamp } : charging interrupted - cable unplugged ." )
343- send_energy_summary (total_energy_wh_for_summary )
344- save_last_state ("disconnected" , total_energy_wh_for_summary = total_energy_wh_for_summary , repeat_check = False ) # Reset repeat_check
399+ send_notification (f"🔌 { timestamp } : interrupted." )
400+ send_energy_summary (last_state , total_energy_wh_for_summary )
401+ save_last_state (last_state , "disconnected" , total_energy_wh_for_summary = total_energy_wh_for_summary , repeat_check = False ) # Reset repeat_check
345402 return # Exit after confirming
346403
347404 # 🚨 If normal disconnect detected (idle state), set repeat_check and exit
348405 if total_energy_wh is None and last_state == "idle" :
349406 if not repeat_check : # First detection, re-run once
350407 debug (f"🔌 { timestamp } : cable disconnect detected, verifying..." )
351- save_last_state ("idle" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = notified , repeat_check = True )
408+ save_last_state (last_state , "idle" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = notified , repeat_check = True )
352409 return # Exit, script will retry next run
353410
354411 # Second run, confirmed disconnection
355- send_notification (f"🔌 { timestamp } : cable disconnected." )
356- send_energy_summary (total_energy_wh_for_summary )
357- save_last_state ("disconnected" , total_energy_wh_for_summary = total_energy_wh_for_summary , repeat_check = False ) # Reset repeat_check
412+ send_notification (f"🔌 { timestamp } : disconnected." )
413+ send_energy_summary (last_state , total_energy_wh_for_summary )
414+ save_last_state (last_state , "disconnected" , total_energy_wh_for_summary = total_energy_wh_for_summary , repeat_check = False ) # Reset repeat_check
358415 return # Exit after confirming
359416
360417 # 🚀 Handle cable reconnection
361418 if last_state == "disconnected" and total_energy_wh is not None :
362- send_notification (f"🔌 { timestamp } : cable connected." )
363- save_last_state ("idle" , stored_power = stored_power , repeat_check = False )
419+ send_notification (f"🔌 { timestamp } : connected." )
420+ # save_last_state(last_state, "idle", stored_power=stored_power, repeat_check=False)
421+ # redundant call of save_last_state() ?
364422
365423 # 🔋 Determine new state
366424 new_state = "idle" if not isinstance (charging_rate , (int , float )) or charging_rate < 1.0 else "charging"
367425
368426 # 📌 Store latest total energy for summary
369427 if total_energy_wh is not None :
370- save_last_state (new_state , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh , notified = notified , repeat_check = False , start_time = start_time )
428+ save_last_state (last_state , new_state , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh , notified = notified , repeat_check = False , start_time = start_time )
371429
372430 # ⚡ Handle charging start
373431 if last_state != "charging" and new_state == "charging" :
374- send_notification (f"🪫 { timestamp } : charging started." )
375- save_last_state ("charging" , stored_power = total_energy_wh , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = False , start_time = current_time )
432+ send_notification (f"🪫 { timestamp } : started." )
433+ save_last_state (last_state , "charging" , stored_power = total_energy_wh , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = False , start_time = current_time )
376434
377435 # ⚡ Notify charging rate once per session
378436 if last_state == "charging" and charging_rate > 0 and not notified :
379- send_notification (f"⚡ { timestamp } : charging rate { charging_rate } kW" )
380- save_last_state ("charging" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = True , start_time = start_time )
437+ send_notification (f"⚡ { timestamp } : rate { charging_rate } kW" )
438+ save_last_state (last_state , "charging" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh_for_summary , notified = True , start_time = start_time )
381439
382440 # 🔋 Handle charging stop
383441 if last_state == "charging" and new_state == "idle" :
384- send_notification (f"🔋 { timestamp } : charging stopped." )
385- save_last_state ("idle" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh , start_time = start_time )
442+ send_notification (f"🔋 { timestamp } : stopped." )
443+ save_last_state (last_state , "idle" , stored_power = stored_power , total_energy_wh_for_summary = total_energy_wh , start_time = start_time )
386444
387445 if total_energy_wh is not None and start_time :
388446 elapsed_time = max (current_time - start_time , 60 )
0 commit comments