Files
steve-reporting/steve_transaction_report.py
2025-10-30 19:48:06 +01:00

505 lines
22 KiB
Python
Executable File

#!/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,
price_host: str = None, price_port: int = None, price_user: str = None, price_password: str = None):
"""Initialiseer database connectie"""
self.config = {
'host': host,
'database': database,
'user': user,
'password': password,
'port': port
}
# Prijzen database config (gebruik SteVe config als fallback)
self.price_config = {
'host': price_host or host,
'port': price_port or port,
'user': price_user or user,
'password': price_password or password
}
self.conn = None
self.cursor = None
self.price_conn = None
self.price_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()
if hasattr(self, 'price_cursor') and self.price_cursor:
self.price_cursor.close()
if hasattr(self, 'price_conn') and self.price_conn:
self.price_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 get_price(self, timestamp: datetime, provider: str, price_db: str) -> Optional[float]:
"""Haal prijs op voor een specifieke timestamp en provider"""
# Maak verbinding met prijzen database indien nodig
if not hasattr(self, 'price_conn') or self.price_conn is None:
try:
# Gebruik price_config met de juiste database naam
price_config = self.price_config.copy()
price_config['database'] = price_db
self.price_conn = mysql.connector.connect(**price_config)
self.price_cursor = self.price_conn.cursor(dictionary=True)
print(f"✓ Verbonden met prijzen database {price_db} op {price_config['host']}")
except mysql.connector.Error as err:
print(f"⚠ Waarschuwing: Kan geen verbinding maken met prijzen database: {err}")
return None
# Round timestamp naar het uur
hour_timestamp = timestamp.replace(minute=0, second=0, microsecond=0)
# Probeer eerst dynamic_price_data (voor historische en vandaag)
query = """
SELECT price
FROM dynamic_price_data
WHERE datetime = %s AND provider_code = %s
LIMIT 1
"""
try:
self.price_cursor.execute(query, (hour_timestamp, provider))
result = self.price_cursor.fetchone()
if result:
return float(result['price'])
except mysql.connector.Error:
pass
# Zo niet, probeer dynamic_price_data_tommorow (alleen NextEnergy)
if provider == 'NE':
query = """
SELECT price
FROM dynamic_price_data_tommorow
WHERE datetime = %s
LIMIT 1
"""
try:
self.price_cursor.execute(query, (hour_timestamp,))
result = self.price_cursor.fetchone()
if result:
return float(result['price'])
except mysql.connector.Error:
pass
return None
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, provider: str = 'NE', price_db: str = 'alfen'):
"""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: {start_value:.3f} kWh")
print(f"Einde: {stop_value:.3f} kWh")
print(f"Totaal geladen: {total_consumption:.2f} kWh")
if transaction['stop_reason']:
print(f"Stop: {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 EN KOSTEN PER UUR:")
print("-" * 80)
print(f"{'Uur':<12} {'Start':<10} {'Eind':<10} {'kWh':<8} {'€/kWh':<11} {'Kosten':<10}")
print("-" * 80)
total_detailed = 0
total_cost = 0
hours_with_price = 0
hours_without_price = 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')
# Haal prijs op voor dit uur
price = self.get_price(hour_dt, provider, price_db)
if price is not None:
cost = hour['consumption'] * price
total_cost += cost
hours_with_price += 1
price_str = f"{price:.5f}"
cost_str = f"{cost:.2f}"
else:
hours_without_price += 1
price_str = "n.v.t."
cost_str = "n.v.t."
print(f"{hour_display:<12} {hour['start_value']:<10.3f} "
f"{hour['end_value']:<10.3f} {hour['consumption']:<8.3f} "
f"{price_str:<11} {cost_str:<10}")
total_detailed += hour['consumption']
print("-" * 80)
cost_total_str = f"{total_cost:.2f}" if hours_without_price == 0 else f"{total_cost:.2f}*"
print(f"{'TOTAAL':<12} {hourly_data[0]['start_value']:<10.3f} "
f"{hourly_data[-1]['end_value']:<10.3f} {total_detailed:<8.3f} "
f"{'':11} {cost_total_str:<10}")
# 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 uren: {len(active_hours)}")
print(f"Provider: {provider}")
avg_per_hour = sum(h['consumption'] for h in active_hours) / len(active_hours)
print(f"Gemiddeld/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')} ({max_hour['consumption']:.2f} kWh)")
# Vind duurste en goedkoopste uur
hours_with_costs = []
for hour in hourly_data:
if hour['consumption'] > 0.01:
hour_dt = datetime.strptime(hour['hour'], '%Y-%m-%d %H:00')
price = self.get_price(hour_dt, provider, price_db)
if price is not None:
hours_with_costs.append((hour_dt, price, hour['consumption']))
if hours_with_costs:
most_expensive = max(hours_with_costs, key=lambda x: x[1])
cheapest = min(hours_with_costs, key=lambda x: x[1])
print(f"Duurste uur: {most_expensive[0].strftime('%d-%m %H:00')} "
f"(€{most_expensive[1]:.5f}/kWh)")
print(f"Goedkoopste uur: {cheapest[0].strftime('%d-%m %H:00')} "
f"(€{cheapest[1]:.5f}/kWh)")
# Totale kosten
print(f"\nTOTAALE KOSTEN: {cost_total_str}")
if hours_without_price > 0:
print(f"* {hours_without_price} uur zonder prijsdata")
# Gemiddelde prijs
if hours_with_price > 0:
avg_price = total_cost / total_detailed if total_detailed > 0 else 0
print(f"Gemiddelde prijs: €{avg_price:.5f}/kWh")
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" + "=" * 80)
print(f"OVERZICHT VAN LAATSTE {len(transactions)} TRANSACTIES")
print("=" * 80)
print(f"{'ID':<4} {'Start':<17} {'Duur':<10} {'kWh':<8} {'Laadpaal':<16} {'RFID':<12}")
print("-" * 80)
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'][:14] if t['charge_box_id'] else "?"
rfid = t['id_tag'][:10] if t['id_tag'] else "?"
print(f"{trans_id:<4} {start:<17} {duration_str:<10} {kwh:<8.2f} {charge_box:<16} {rfid:<12}")
print("=" * 80)
print(f"\nGebruik --transaction <ID> 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 --host 192.168.178.201 --port 3307 --password 'geheim' --list
# Toon gedetailleerd rapport van transactie 1 met NextEnergy prijzen
python3 steve_transaction_report.py --host 192.168.178.201 --port 3307 --password 'geheim' --transaction 1
# Gebruik aparte credentials voor prijzen database
python3 steve_transaction_report.py --host 192.168.178.201 --port 3307 --password 'geheim' \
--price-host 192.168.178.201 --price-port 3307 --price-user prijzen --price-password 'geheim2' --transaction 1
# Toon rapport met andere provider (bijv. 'AA' voor AllAbout)
python3 steve_transaction_report.py --host 192.168.178.201 --port 3307 --password 'geheim' --transaction 1 --provider AA
# Toon laatste 20 transacties in lijst
python3 steve_transaction_report.py --host 192.168.178.201 --port 3307 --password 'geheim' --list --limit 20
# Toon alle transacties gedetailleerd (laatste 10)
python3 steve_transaction_report.py --host 192.168.178.201 --port 3307 --password 'geheim'
"""
)
# 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 compact overzicht van transacties (tabel format)')
parser.add_argument('--transaction', type=int, metavar='ID', help='Toon gedetailleerd rapport van specifieke transactie ID')
parser.add_argument('--limit', type=int, default=10, help='Aantal transacties (default: 10)')
parser.add_argument('--simple', action='store_true', help='Eenvoudig rapport zonder uurdetails')
# Prijzen opties
parser.add_argument('--provider', default='NE', help='Energie provider code (default: NE voor NextEnergy)')
parser.add_argument('--price-db', default='alfen', help='Database naam voor prijzen (default: alfen)')
parser.add_argument('--price-host', default=None, help='Database host voor prijzen (default: zelfde als --host)')
parser.add_argument('--price-port', type=int, default=None, help='Database poort voor prijzen (default: zelfde als --port)')
parser.add_argument('--price-user', default=None, help='Database gebruiker voor prijzen (default: zelfde als --user)')
parser.add_argument('--price-password', default=None, help='Database wachtwoord voor prijzen (default: zelfde als --password)')
args = parser.parse_args()
# Maak reporter object
reporter = SteVeReporter(
host=args.host,
database=args.database,
user=args.user,
password=args.password,
port=args.port,
price_host=args.price_host,
price_port=args.price_port,
price_user=args.price_user,
price_password=args.price_password
)
try:
reporter.connect()
if args.transaction:
# Toon specifieke transactie
transactions = reporter.get_transactions(transaction_id=args.transaction)
if transactions:
reporter.print_transaction_report(transactions[0], detailed=not args.simple,
provider=args.provider, price_db=args.price_db)
else:
print(f"✗ Transactie {args.transaction} niet gevonden")
sys.exit(1)
elif args.list:
# Toon compact overzicht
reporter.list_transactions(limit=args.limit)
else:
# Geen specifieke optie: toon alle transacties gedetailleerd
transactions = reporter.get_transactions(limit=args.limit)
if not transactions:
print("Geen transacties gevonden.")
else:
print(f"\n{'=' * 80}")
print(f"GEDETAILLEERDE RAPPORTEN VAN LAATSTE {len(transactions)} TRANSACTIES")
print(f"{'=' * 80}")
for i, trans in enumerate(transactions):
if i > 0:
print("\n\n")
reporter.print_transaction_report(trans, detailed=not args.simple,
provider=args.provider, price_db=args.price_db)
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()