Add API interaction for charger data retrieval and CSV export functionality

- Implemented login and logout endpoints for user authentication.
- Added functionality to fetch charger information and transaction data.
- Introduced methods to parse and save raw API data into CSV format.
- Enhanced logging for debugging and tracking API responses.
- Included environment variable loading for sensitive configuration.
This commit is contained in:
Mark Kors
2026-07-12 17:42:14 +02:00
parent 5f0f30b788
commit 5476b5b15d
5 changed files with 14726 additions and 25 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -48,31 +48,61 @@ def fetch_page(offset):
def get_all_raw(): def get_all_raw():
"""Haalt alle transactiepagina's op via paginering. """Haalt alle transactiepagina's op via paginering.
Stopt zodra de record-nummers terugvallen (circulaire buffer bereikt). Stopt zodra de record-nummers terugvallen (circulaire buffer bereikt).
Een niet-afgesloten transactie kan een "gat" in de recordnummering
achterlaten (een gereserveerde maar nooit weggeschreven slot). Een
pagina die op zo'n gat begint komt leeg terug, ook al staat er verderop
gewoon weer geldige data. Daarom wordt bij een lege pagina niet meteen
gestopt, maar met oplopende stappen verderop geprobeerd of de data
doorloopt, voordat we concluderen dat het echt het einde is.
""" """
import sys, itertools debug = os.getenv("DEBUG") == "1"
spin = itertools.cycle('|/-\\') MAX_GAP = 5000 # verst dat we een leeg/kapot record proberen te overspringen
all_raw = "" all_raw = ""
offset = 0 offset = 0
while True: while True:
#sys.stdout.write(next(spin) + '\b')
#sys.stdout.flush()
raw = fetch_page(offset) raw = fetch_page(offset)
stripped = raw.strip().rstrip('}').strip() stripped = raw.strip().rstrip('}').strip()
if debug:
print(f"\n[DEBUG] offset={offset} len(raw)={len(raw)} stripped_preview={stripped[:80]!r}")
if not stripped or stripped in ('{"version":2,', '{"version":2'): if not stripped or stripped in ('{"version":2,', '{"version":2'):
if debug:
print(f"[DEBUG] stop: lege/afgesloten pagina bij offset={offset}")
break break
offsets = re.findall(r'^(\d+)_\w+:', raw, re.MULTILINE)
if not offsets:
# Mogelijk een gat door een niet-afgesloten transactie: probeer
# verderop met oplopende stappen of de data daar doorloopt.
gap = 1
while gap <= MAX_GAP:
probe_offset = offset + gap
probe_raw = fetch_page(probe_offset)
probe_offsets = re.findall(r'^(\d+)_\w+:', probe_raw, re.MULTILINE)
if debug:
print(f"[DEBUG] gat-probe offset={probe_offset} -> {'data gevonden' if probe_offsets else 'nog leeg'}")
if probe_offsets:
offset, raw, offsets = probe_offset, probe_raw, probe_offsets
break
gap *= 4
else:
if debug:
print(f"[DEBUG] stop: geen data gevonden binnen {MAX_GAP} na offset={offset}")
break
all_raw += raw 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 next_offset = max(int(x) for x in offsets) + 1
if debug:
print(f"[DEBUG] gevonden offsets {min(int(x) for x in offsets)}..{max(int(x) for x in offsets)}, next_offset={next_offset}")
if next_offset <= offset: if next_offset <= offset:
if debug:
print(f"[DEBUG] stop: next_offset({next_offset}) <= offset({offset}) — geen voortgang")
break break
offset = next_offset offset = next_offset
@@ -85,7 +115,8 @@ def get_all_raw():
def parse_transactions(raw): def parse_transactions(raw):
transactions = [] transactions = []
current_tx = None current_tx = None
stop_parsing = False last_offset = None
seen_starts = set()
# Verwijder pagina-headers eerst, zodat }{"version":2,305085_ correct wordt gesplitst # Verwijder pagina-headers eerst, zodat }{"version":2,305085_ correct wordt gesplitst
raw = re.sub(r'\{"version":\d+,\s*', '', raw) raw = re.sub(r'\{"version":\d+,\s*', '', raw)
@@ -94,9 +125,6 @@ def parse_transactions(raw):
raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw) raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw)
for line in raw.splitlines(): for line in raw.splitlines():
if stop_parsing:
break
line = line.strip().rstrip('}').strip() line = line.strip().rstrip('}').strip()
if not line: if not line:
continue continue
@@ -105,7 +133,17 @@ def parse_transactions(raw):
if not match: if not match:
continue continue
_, record_type, data = match.groups() offset_str, record_type, data = match.groups()
offset = int(offset_str)
# Cirkelbuffer: recordnummers lopen monotoon op. Val terug = oude/herhaalde
# data (buffer-wraparound) → stop met parsen. Timestamps zijn hiervoor niet
# betrouwbaar: na een onverwachte reset (bv. door een niet-afgesloten
# transactie) kan de klok van de paal tijdelijk terugvallen, waardoor latere,
# geldige transacties anders ten onrechte als "oude data" werden gezien.
if last_offset is not None and offset < last_offset:
break
last_offset = offset
if record_type == 'txstart2': if record_type == 'txstart2':
m = re.match( m = re.match(
@@ -116,11 +154,16 @@ def parse_transactions(raw):
) )
if m: if m:
start_time = datetime.strptime(m.group(3), '%Y-%m-%d %H:%M:%S') 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 # Inhoudelijke dedup: de paal kan bij het overspringen van een gat
latest_start = max((tx['start_time'] for tx in transactions), default=None) # (zie get_all_raw) oude data teruggeven onder een nieuw, oplopend
if latest_start and start_time < latest_start: # offset-label. Komt dezelfde sessie-signatuur al voor, dan is de
# cirkelbuffer echt rond → stop met parsen.
start_key = (m.group(1), int(m.group(2)), start_time, float(m.group(4)))
if start_key in seen_starts:
break break
seen_starts.add(start_key)
current_tx = { current_tx = {
'rfid': m.group(1), 'rfid': m.group(1),
'socket': int(m.group(2)), 'socket': int(m.group(2)),
@@ -227,23 +270,33 @@ def save_as_csv(raw, device_id, filename=None):
raw = re.sub(r'\{"version":\d+,\s*', '', raw) raw = re.sub(r'\{"version":\d+,\s*', '', raw)
raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw) raw = re.sub(r'\}\s*(\d+_)', r'\n\1', raw)
latest_start = None last_offset = None
seen_starts = set()
for line in raw.splitlines(): for line in raw.splitlines():
line = line.rstrip('}').strip() line = line.rstrip('}').strip()
if not line: if not line:
continue continue
# Cirkelbuffer: recordnummers lopen monotoon op; val terug = oude/herhaalde data
offset_match = re.match(r'^(\d+)_', line)
if offset_match:
offset = int(offset_match.group(1))
if last_offset is not None and offset < last_offset:
break
last_offset = offset
# Verwijder offset-nummers: "76_mv:" → "mv:", "0_txstart2:" → "txstart2:" # Verwijder offset-nummers: "76_mv:" → "mv:", "0_txstart2:" → "txstart2:"
line = re.sub(r'^\d+_', '', line) line = re.sub(r'^\d+_', '', line)
# Cirkelbuffer: stop als txstart2 terug in de tijd gaat # Inhoudelijke dedup: de paal kan bij een gat in de nummering oude data
# teruggeven onder een nieuw, oplopend offset-label (zie get_all_raw).
# Komt dezelfde txstart2-payload al voor, dan is de cirkelbuffer echt
# rond → stop.
if line.startswith('txstart2:'): if line.startswith('txstart2:'):
m = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) payload = line[len('txstart2:'):].strip()
if m: if payload in seen_starts:
start_time = datetime.strptime(m.group(1), '%Y-%m-%d %H:%M:%S') break
if latest_start and start_time < latest_start: seen_starts.add(payload)
break
latest_start = start_time
lines.append(line) lines.append(line)