Files
import_alfen_transactions/uitlezen_laadpaal.py
2026-04-09 09:42:46 +02:00

302 lines
11 KiB
Python

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 _probe_max_offset():
"""Doe één request met een enorm offset-nummer om het hoogste record-nummer te vinden."""
raw = fetch_page(999_999_999)
offsets = re.findall(r'^(\d+)_\w+:', raw, re.MULTILINE)
return max(int(x) for x in offsets) if offsets else None
def _print_progress(current, total, width=40):
import sys
pct = min(current / total, 1.0)
filled = int(width * pct)
bar = '#' * filled + '-' * (width - filled)
line = f" [{bar}] {pct*100:5.1f}% (record {current}/{total})"
sys.stdout.write(f"\r{line:<70}")
sys.stdout.flush()
def get_all_raw():
"""Haalt alle transactiepagina's op via paginering.
Stopt zodra de record-nummers terugvallen (circulaire buffer bereikt).
"""
import sys
print(" Bepalen totaal aantal records...", end='', flush=True)
max_record = _probe_max_offset()
if max_record:
print(f" max record = {max_record}")
else:
print(" onbekend, toon teller")
all_raw = ""
offset = 0
while True:
raw = fetch_page(offset)
stripped = raw.strip().rstrip('}').strip()
if not stripped or stripped in ('{"version":2,', '{"version":2'):
if max_record:
_print_progress(max_record, max_record)
print("\n Klaar.")
break
all_raw += raw
offsets = re.findall(r'^(\d+)_\w+:', raw, re.MULTILINE)
if not offsets:
print("\n Klaar.")
break
next_offset = max(int(x) for x in offsets) + 1
if max_record:
_print_progress(next_offset, max_record)
else:
sys.stdout.write(f"\r Record {next_offset}...")
sys.stdout.flush()
# Circulaire buffer: nummers vallen terug → we hebben alles gehad
if next_offset <= offset:
print("\n Klaar (circulaire buffer).")
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...")
raw = get_all_raw()
transactions = parse_transactions(raw)
print_summary(transactions)
save_as_csv(raw, device_id)
finally:
logout()