From 57a681ae79f0156c71667636381451d39050fa25 Mon Sep 17 00:00:00 2001 From: Mark Kors Date: Wed, 29 Oct 2025 15:50:38 +0100 Subject: [PATCH] initial commit --- steve_transaction_report.py | 365 ++++++++++++++++++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100755 steve_transaction_report.py diff --git a/steve_transaction_report.py b/steve_transaction_report.py new file mode 100755 index 0000000..7be55c4 --- /dev/null +++ b/steve_transaction_report.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +SteVe Transaction Report Generator +Genereert gedetailleerde rapporten van laadtransacties uit de SteVe database +""" + +import mysql.connector +from datetime import datetime, timedelta +import argparse +import sys +from typing import List, Dict, Optional + +class SteVeReporter: + def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): + """Initialiseer database connectie""" + self.config = { + 'host': host, + 'database': database, + 'user': user, + 'password': password, + 'port': port + } + self.conn = None + self.cursor = None + + def connect(self): + """Maak verbinding met de database""" + try: + self.conn = mysql.connector.connect(**self.config) + self.cursor = self.conn.cursor(dictionary=True) + print(f"✓ Verbonden met database {self.config['database']} op {self.config['host']}") + except mysql.connector.Error as err: + print(f"✗ Database fout: {err}") + sys.exit(1) + + def disconnect(self): + """Sluit database verbinding""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() + + def get_transactions(self, limit: Optional[int] = None, transaction_id: Optional[int] = None) -> List[Dict]: + """Haal transacties op uit de database""" + if transaction_id: + query = """ + SELECT + t.transaction_pk, + t.id_tag, + t.start_timestamp, + t.stop_timestamp, + t.start_value, + t.stop_value, + t.stop_reason, + cb.charge_box_id, + cb.description as charge_box_description, + c.connector_id + FROM transaction t + JOIN connector c ON t.connector_pk = c.connector_pk + JOIN charge_box cb ON c.charge_box_id = cb.charge_box_id + WHERE t.transaction_pk = %s + """ + self.cursor.execute(query, (transaction_id,)) + else: + query = """ + SELECT + t.transaction_pk, + t.id_tag, + t.start_timestamp, + t.stop_timestamp, + t.start_value, + t.stop_value, + t.stop_reason, + cb.charge_box_id, + cb.description as charge_box_description, + c.connector_id + FROM transaction t + JOIN connector c ON t.connector_pk = c.connector_pk + JOIN charge_box cb ON c.charge_box_id = cb.charge_box_id + ORDER BY t.start_timestamp DESC + """ + if limit: + query += f" LIMIT {limit}" + self.cursor.execute(query) + + return self.cursor.fetchall() + + def get_meter_values(self, transaction_pk: int) -> List[Dict]: + """Haal alle meterwaarden op voor een specifieke transactie""" + query = """ + SELECT + value_timestamp, + value, + reading_context, + measurand, + location, + unit + FROM connector_meter_value + WHERE transaction_pk = %s + ORDER BY value_timestamp ASC + """ + self.cursor.execute(query, (transaction_pk,)) + return self.cursor.fetchall() + + def get_user_info(self, id_tag: str) -> Optional[Dict]: + """Haal gebruikersinformatie op""" + query = """ + SELECT u.first_name, u.last_name, u.e_mail + FROM user u + JOIN user_ocpp_tag uot ON u.user_pk = uot.user_pk + JOIN ocpp_tag ot ON uot.ocpp_tag_pk = ot.ocpp_tag_pk + WHERE ot.id_tag = %s + """ + self.cursor.execute(query, (id_tag,)) + return self.cursor.fetchone() + + def analyze_hourly_consumption(self, meter_values: List[Dict]) -> List[Dict]: + """Analyseer verbruik per uur""" + if not meter_values: + return [] + + hourly_data = {} + + for mv in meter_values: + timestamp = mv['value_timestamp'] + value = float(mv['value']) + hour_key = timestamp.strftime('%Y-%m-%d %H:00') + + if hour_key not in hourly_data: + hourly_data[hour_key] = { + 'hour': hour_key, + 'start_value': value, + 'end_value': value, + 'start_time': timestamp, + 'end_time': timestamp + } + else: + hourly_data[hour_key]['end_value'] = value + hourly_data[hour_key]['end_time'] = timestamp + + # Bereken consumptie per uur + result = [] + for hour_key in sorted(hourly_data.keys()): + hour_data = hourly_data[hour_key] + consumption = hour_data['end_value'] - hour_data['start_value'] + result.append({ + 'hour': hour_key, + 'start_value': hour_data['start_value'], + 'end_value': hour_data['end_value'], + 'consumption': consumption, + 'start_time': hour_data['start_time'], + 'end_time': hour_data['end_time'] + }) + + return result + + def print_transaction_report(self, transaction: Dict, detailed: bool = True): + """Print een geformatteerd rapport van een transactie""" + print("\n" + "=" * 80) + print(f"LAADSESSIE RAPPORT - Transactie #{transaction['transaction_pk']}") + print("=" * 80) + + # Basis informatie + print(f"Laadpaal: {transaction['charge_box_id']}") + if transaction['charge_box_description']: + print(f" {transaction['charge_box_description']}") + print(f"Connector: {transaction['connector_id']}") + print(f"RFID Tag: {transaction['id_tag']}") + + # Gebruikersinformatie + user_info = self.get_user_info(transaction['id_tag']) + if user_info and user_info['first_name']: + user_name = f"{user_info['first_name']}" + if user_info['last_name']: + user_name += f" {user_info['last_name']}" + print(f"Gebruiker: {user_name}") + + # Tijden + start_time = transaction['start_timestamp'] + stop_time = transaction['stop_timestamp'] + + if start_time: + print(f"Start: {start_time.strftime('%d-%m-%Y %H:%M:%S')}") + if stop_time: + print(f"Einde: {stop_time.strftime('%d-%m-%Y %H:%M:%S')}") + if start_time: + duration = stop_time - start_time + hours = duration.total_seconds() / 3600 + print(f"Duur: {int(hours)}u {int((hours % 1) * 60)}m") + + # Verbruik + start_value = float(transaction['start_value']) if transaction['start_value'] else 0 + stop_value = float(transaction['stop_value']) if transaction['stop_value'] else 0 + total_consumption = stop_value - start_value + + print(f"\nStart meterstand: {start_value:.3f} kWh") + print(f"Eind meterstand: {stop_value:.3f} kWh") + print(f"Totaal geladen: {total_consumption:.2f} kWh") + + if transaction['stop_reason']: + print(f"Stop reden: {transaction['stop_reason']}") + + print("=" * 80) + + # Gedetailleerde analyse per uur + if detailed and stop_time: + meter_values = self.get_meter_values(transaction['transaction_pk']) + + if meter_values: + hourly_data = self.analyze_hourly_consumption(meter_values) + + if hourly_data: + print("\nVERBRUIK PER UUR:") + print("-" * 80) + print(f"{'Uur':<20} {'Start (kWh)':<15} {'Eind (kWh)':<15} {'Geladen (kWh)':<15}") + print("-" * 80) + + total_detailed = 0 + for hour in hourly_data: + hour_dt = datetime.strptime(hour['hour'], '%Y-%m-%d %H:00') + hour_display = hour_dt.strftime('%d-%m %H:%M') + + print(f"{hour_display:<20} {hour['start_value']:<15.3f} " + f"{hour['end_value']:<15.3f} {hour['consumption']:<15.3f}") + total_detailed += hour['consumption'] + + print("-" * 80) + print(f"{'TOTAAL':<20} {hourly_data[0]['start_value']:<15.3f} " + f"{hourly_data[-1]['end_value']:<15.3f} {total_detailed:<15.3f}") + + # Statistieken + active_hours = [h for h in hourly_data if h['consumption'] > 0.01] + if active_hours: + print("\n" + "=" * 80) + print("STATISTIEKEN:") + print("-" * 80) + print(f"Actieve laaduren: {len(active_hours)}") + avg_per_hour = sum(h['consumption'] for h in active_hours) / len(active_hours) + print(f"Gemiddeld per actief uur: {avg_per_hour:.2f} kWh") + + max_hour = max(hourly_data, key=lambda x: x['consumption']) + if max_hour['consumption'] > 0: + max_time = datetime.strptime(max_hour['hour'], '%Y-%m-%d %H:00') + print(f"Piekuur: {max_time.strftime('%d-%m %H:00')} " + f"({max_hour['consumption']:.2f} kWh)") + + # Bereken geschatte kosten (indicatief) + print(f"\nGeschatte kosten (€0.30/kWh): €{total_consumption * 0.30:.2f}") + print(f"Geschatte kosten (€0.10/kWh): €{total_consumption * 0.10:.2f}") + + print("=" * 80) + + def list_transactions(self, limit: int = 10): + """Toon een overzicht van recente transacties""" + transactions = self.get_transactions(limit=limit) + + if not transactions: + print("Geen transacties gevonden.") + return + + print("\n" + "=" * 100) + print(f"OVERZICHT VAN LAATSTE {len(transactions)} TRANSACTIES") + print("=" * 100) + print(f"{'ID':<6} {'Start':<20} {'Duur':<12} {'kWh':<10} {'Laadpaal':<20} {'RFID':<15}") + print("-" * 100) + + for t in transactions: + trans_id = t['transaction_pk'] + start = t['start_timestamp'].strftime('%d-%m-%Y %H:%M') if t['start_timestamp'] else 'Onbekend' + + if t['start_timestamp'] and t['stop_timestamp']: + duration = t['stop_timestamp'] - t['start_timestamp'] + hours = duration.total_seconds() / 3600 + duration_str = f"{int(hours)}u {int((hours % 1) * 60)}m" + else: + duration_str = "Actief" if not t['stop_timestamp'] else "?" + + start_val = float(t['start_value']) if t['start_value'] else 0 + stop_val = float(t['stop_value']) if t['stop_value'] else 0 + kwh = stop_val - start_val + + charge_box = t['charge_box_id'][:18] if t['charge_box_id'] else "?" + rfid = t['id_tag'][:13] if t['id_tag'] else "?" + + print(f"{trans_id:<6} {start:<20} {duration_str:<12} {kwh:<10.2f} {charge_box:<20} {rfid:<15}") + + print("=" * 100) + print(f"\nGebruik: python3 {sys.argv[0]} --transaction voor details\n") + + +def main(): + parser = argparse.ArgumentParser( + description='SteVe Transaction Report Generator', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Voorbeelden: + # Toon lijst van laatste 10 transacties + python3 steve_transaction_report.py --list + + # Toon gedetailleerd rapport van transactie 1 + python3 steve_transaction_report.py --transaction 1 + + # Toon laatste 20 transacties + python3 steve_transaction_report.py --list --limit 20 + + # Gebruik andere database credentials + python3 steve_transaction_report.py --host 192.168.1.100 --user steve --password geheim --transaction 1 + """ + ) + + # Database opties + parser.add_argument('--host', default='localhost', help='Database host (default: localhost)') + parser.add_argument('--port', type=int, default=3306, help='Database poort (default: 3306)') + parser.add_argument('--database', default='stevedb', help='Database naam (default: stevedb)') + parser.add_argument('--user', default='steve', help='Database gebruiker (default: steve)') + parser.add_argument('--password', required=True, help='Database wachtwoord') + + # Actie opties + parser.add_argument('--list', action='store_true', help='Toon overzicht van transacties') + parser.add_argument('--transaction', type=int, metavar='ID', help='Toon gedetailleerd rapport van transactie ID') + parser.add_argument('--limit', type=int, default=10, help='Aantal transacties bij --list (default: 10)') + parser.add_argument('--simple', action='store_true', help='Eenvoudig rapport zonder uurdetails') + + args = parser.parse_args() + + # Minimaal één actie nodig + if not args.list and not args.transaction: + parser.print_help() + sys.exit(1) + + # Maak reporter object + reporter = SteVeReporter( + host=args.host, + database=args.database, + user=args.user, + password=args.password, + port=args.port + ) + + try: + reporter.connect() + + if args.list: + reporter.list_transactions(limit=args.limit) + + if args.transaction: + transactions = reporter.get_transactions(transaction_id=args.transaction) + if transactions: + reporter.print_transaction_report(transactions[0], detailed=not args.simple) + else: + print(f"✗ Transactie {args.transaction} niet gevonden") + sys.exit(1) + + except KeyboardInterrupt: + print("\n\nAfgebroken door gebruiker") + sys.exit(0) + except Exception as e: + print(f"✗ Fout: {e}") + sys.exit(1) + finally: + reporter.disconnect() + + +if __name__ == '__main__': + main()