import requests import urllib3 from datetime import datetime import re import os from dotenv import load_dotenv load_dotenv() urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ── Configuratie ────────────────────────────────────────────── HOST = os.getenv("HOST", "https://192.168.178.184") USERNAME = os.getenv("CHARGER_USERNAME", "admin") PASSWORD = os.getenv("CHARGER_PASSWORD") # ───────────────────────────────────────────────────────────── session = requests.Session() session.verify = False # ── Communicatie ────────────────────────────────────────────── def login(): session.post(f"{HOST}/api/login", json={ "username": USERNAME, "password": PASSWORD }).raise_for_status() print("Ingelogd") def logout(): session.post(f"{HOST}/api/logout", json={ "username": USERNAME, "password": PASSWORD }) print("Uitgelogd") def get_info(): r = session.get(f"{HOST}/api/info") r.raise_for_status() return r.json() def fetch_page(offset): r = session.get(f"{HOST}/api/transactions", params={"offset": offset}) r.raise_for_status() return r.text def get_all_raw(): """Haalt alle transactiepagina's op via paginering. Stopt zodra de record-nummers terugvallen (circulaire buffer bereikt). """ import sys, itertools spin = itertools.cycle('|/-\\') all_raw = "" offset = 0 while True: #sys.stdout.write(next(spin) + '\b') #sys.stdout.flush() raw = fetch_page(offset) stripped = raw.strip().rstrip('}').strip() if not stripped or stripped in ('{"version":2,', '{"version":2'): break all_raw += raw offsets = re.findall(r'^(\d+)_\w+:', raw, re.MULTILINE) if not offsets: break next_offset = max(int(x) for x in offsets) + 1 if next_offset <= offset: break offset = next_offset return all_raw # ── Parser ──────────────────────────────────────────────────── def parse_transactions(raw): transactions = [] current_tx = None stop_parsing = False # Verwijder pagina-headers eerst, zodat }{"version":2,305085_ correct wordt gesplitst raw = re.sub(r'\{"version":\d+,\s*', '', raw) # Records worden soms op één regel samengevoegd: "...N}305085_txstop2:..." # Splits op } gevolgd door optionele spaties en een recordnummer raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw) for line in raw.splitlines(): if stop_parsing: break line = line.strip().rstrip('}').strip() if not line: continue match = re.match(r'^(\d+)_(\w+):\s*(.+)$', line) if not match: continue _, record_type, data = match.groups() if record_type == 'txstart2': m = re.match( r'id (0x[0-9a-fA-F]+), socket (\d+), ' r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ' r'([\d.]+)kWh (\S+)', data ) if m: start_time = datetime.strptime(m.group(3), '%Y-%m-%d %H:%M:%S') # Cirkelbuffer: als de starttijd eerder is dan de hoogste geziene starttijd, # zijn we in herhaalde data beland → stop met parsen latest_start = max((tx['start_time'] for tx in transactions), default=None) if latest_start and start_time < latest_start: break current_tx = { 'rfid': m.group(1), 'socket': int(m.group(2)), 'start_time': start_time, 'start_kwh': float(m.group(4)), 'tag': m.group(5), 'end_time': None, 'end_kwh': None, 'charged_kwh': None, 'status': 'lopend', 'measurements': [] } transactions.append(current_tx) elif record_type == 'txstop2' and current_tx: # Correct record type — was eerder fout als 'txend' m = re.match( r'id (0x[0-9a-fA-F]+), socket (\d+), ' r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ' r'([\d.]+)kWh', data ) if m: current_tx['end_time'] = datetime.strptime(m.group(3), '%Y-%m-%d %H:%M:%S') current_tx['end_kwh'] = float(m.group(4)) current_tx['status'] = 'afgesloten' current_tx = None # Sessie klaar elif record_type == 'mv' and current_tx: # Sla korte mv-regels over (geen volledige meetwaarden) m = re.match( r'socket \d+, (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ' r'([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)', data ) if m: meting = { 'time': datetime.strptime(m.group(1), '%Y-%m-%d %H:%M:%S'), 'current_a': float(m.group(2)), 'energy_wh': float(m.group(3)), 'freq_hz': float(m.group(4)), 'power_w': float(m.group(5)), 'power_factor': float(m.group(6)), 'temp_c': float(m.group(7)), } current_tx['measurements'].append(meting) # Alleen bijwerken als sessie nog niet afgesloten via txstop2 # en de meettijd na de starttijd ligt (circulaire buffer kan oude data bevatten) if current_tx['status'] == 'lopend' and meting['time'] >= current_tx['start_time']: current_tx['end_time'] = meting['time'] current_tx['end_kwh'] = meting['energy_wh'] / 1000 # Geladen kWh berekenen for tx in transactions: if tx['start_kwh'] is not None and tx['end_kwh'] is not None: tx['charged_kwh'] = round(tx['end_kwh'] - tx['start_kwh'], 3) return transactions # ── Rapportage ──────────────────────────────────────────────── def print_summary(transactions): print(f"\n{'='*68}") print(f" Totaal sessies gevonden: {len(transactions)}") print(f"{'='*68}") for i, tx in enumerate(transactions, 1): duur = "?" if tx['start_time'] and tx['end_time']: delta = tx['end_time'] - tx['start_time'] uur, rest = divmod(int(delta.total_seconds()), 3600) min_ = rest // 60 duur = f"{uur}u {min_:02d}m" kwh_str = f"{tx['charged_kwh']} kWh" if tx['charged_kwh'] is not None else "?" eind_str = tx['end_time'].strftime('%Y-%m-%d %H:%M') if tx['end_time'] else '?' print( f" #{i:<3} {tx['start_time'].strftime('%Y-%m-%d %H:%M')} " f"→ {eind_str} " f"| {kwh_str:>10} " f"| {duur:>7} " f"| {tx['status']}" ) print(f"{'='*68}") totaal = sum(tx['charged_kwh'] for tx in transactions if tx['charged_kwh']) print(f" Totaal geladen: {round(totaal, 3)} kWh") print(f"{'='*68}\n") def save_as_csv(raw, device_id, filename=None): """Schrijft de ruwe API-data naar een CSV in ACE Service Installer formaat.""" from datetime import datetime if filename is None: filename = f"{device_id}_Transactions.csv" now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") lines = [ f"# Device, {device_id}", f"# Generated, {now}", ] # Zelfde normalisatie als parse_transactions raw = re.sub(r'\{"version":\d+,\s*', '', raw) raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw) latest_start = None for line in raw.splitlines(): line = line.rstrip('}').strip() if not line: continue # Verwijder offset-nummers: "76_mv:" → "mv:", "0_txstart2:" → "txstart2:" line = re.sub(r'^\d+_', '', line) # Cirkelbuffer: stop als txstart2 terug in de tijd gaat if line.startswith('txstart2:'): m = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) if m: start_time = datetime.strptime(m.group(1), '%Y-%m-%d %H:%M:%S') if latest_start and start_time < latest_start: break latest_start = start_time lines.append(line) with open(filename, 'w', encoding='utf-8') as f: f.write('\n'.join(lines) + '\n') print(f"Opgeslagen als: {filename}") return filename # ── Main ────────────────────────────────────────────────────── if __name__ == "__main__": info = get_info() device_id = info.get('Identity', 'UNKNOWN') print(f"Paal: {info.get('Model')} | FW: {info.get('FWVersion')} | ID: {info.get('ObjectId')}") login() try: print("\nTransacties ophalen...", end=' ', flush=True) raw = get_all_raw() print("Verwerken...", end=' ', flush=True) transactions = parse_transactions(raw) print("Klaar.\n") print_summary(transactions) print("Opslaan als CSV...", end=' ', flush=True) start_dates = [tx['start_time'] for tx in transactions if tx['start_time']] end_dates = [tx['end_time'] for tx in transactions if tx['end_time']] if start_dates and end_dates: date_from = min(start_dates).strftime('%Y%m%d') date_to = max(end_dates).strftime('%Y%m%d') filename = f"{device_id}_Transactions_{date_from}_{date_to}.csv" else: filename = f"{device_id}_Transactions.csv" save_as_csv(raw, device_id, filename) finally: logout()