#!/usr/bin/env python3
"""
Marstek Venus Modbus -> SQLite
Alle installationsspezifischen Werte (IP, Pfade) stehen in config.ini im selben Ordner.

Cron-Beispiel: */5 * * * * python3 /pfad/zu/battery_collector.py
"""

import configparser
import os
import re
import sqlite3
import subprocess
import sys
import time
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
MODBUS_PY  = os.path.join(SCRIPT_DIR, 'modbus.py')

cfg = configparser.ConfigParser()
cfg.read(os.path.join(SCRIPT_DIR, 'config.ini'))

IP         = cfg.get('modbus', 'ip')
SLAVE_ID   = cfg.getint('modbus', 'slave_id', fallback=1)
READ_DELAY = cfg.getfloat('modbus', 'read_delay', fallback=3.0)
DB_PATH    = cfg.get('database', 'path')

# ── Modbus-Lesen ──────────────────────────────────────────────────────────

def _raw(reg):
    try:
        res = subprocess.run(
            ['python3', MODBUS_PY, IP, str(reg), str(SLAVE_ID)],
            capture_output=True, text=True, timeout=5
        )
        mr = re.search(r'Roh\s*:\s*(\d+)', res.stdout)
        ms = re.search(r'Signed\s*:\s*(-?\d+)', res.stdout)
        return (int(mr.group(1)) if mr else None,
                int(ms.group(1)) if ms else None)
    except Exception:
        return (None, None)

def u16(reg):
    time.sleep(READ_DELAY)
    return _raw(reg)[0]

def i16(reg):
    time.sleep(READ_DELAY)
    return _raw(reg)[1]

def u32(reg):
    hi = u16(reg)
    lo = u16(reg + 1)
    if hi is None or lo is None:
        return None
    return hi * 65536 + lo

def i32(reg):
    v = u32(reg)
    if v is None:
        return None
    return v - 4294967296 if v > 2147483647 else v

def sc(v, factor, decimals=4):
    return None if v is None else round(v * factor, decimals)

# ── Schema ────────────────────────────────────────────────────────────────

CREATE_TABLE = '''CREATE TABLE IF NOT EXISTS readings (
    id                    INTEGER PRIMARY KEY AUTOINCREMENT,
    ts                    TEXT NOT NULL,
    battery_power_w       INTEGER,
    ac_power_w            INTEGER,
    battery_voltage_v     REAL,
    battery_current_a     REAL,
    battery_soc_pct       REAL,
    battery_capacity_kwh  REAL,
    inverter_state        INTEGER,
    ac_voltage_v          REAL,
    ac_current_a          REAL,
    ac_frequency_hz       REAL,
    internal_temp_c       REAL,
    mos1_temp_c           REAL,
    mos2_temp_c           REAL,
    max_cell_temp_c       REAL,
    min_cell_temp_c       REAL,
    max_cell_voltage_v    REAL,
    min_cell_voltage_v    REAL,
    total_charge_kwh      REAL,
    total_discharge_kwh   REAL,
    daily_charge_kwh      REAL,
    daily_discharge_kwh   REAL,
    monthly_charge_kwh    REAL,
    monthly_discharge_kwh REAL,
    cycle_count           INTEGER,
    cell1_v  REAL, cell2_v  REAL, cell3_v  REAL, cell4_v  REAL, cell5_v  REAL,
    cell6_v  REAL, cell7_v  REAL, cell8_v  REAL, cell9_v  REAL, cell10_v REAL,
    cell11_v REAL, cell12_v REAL, cell13_v REAL,
    mppt1_voltage_v REAL, mppt1_current_a REAL, mppt1_power_w REAL,
    mppt2_voltage_v REAL, mppt2_current_a REAL, mppt2_power_w REAL,
    mppt3_voltage_v REAL, mppt3_current_a REAL, mppt3_power_w REAL,
    mppt4_voltage_v REAL, mppt4_current_a REAL, mppt4_power_w REAL,
    wifi_signal_dbm INTEGER,
    work_mode       INTEGER,
    force_mode      INTEGER,
    max_charge_w    INTEGER,
    max_discharge_w INTEGER
)'''

CREATE_INDEX = 'CREATE INDEX IF NOT EXISTS idx_ts ON readings (ts)'

# ── Sammeln ───────────────────────────────────────────────────────────────

def set_field(d, key, value):
    d[key] = value
    print(f'  {key} = {value}', flush=True)
    return value

def collect():
    db = sqlite3.connect(DB_PATH)
    db.execute(CREATE_TABLE)
    db.execute(CREATE_INDEX)
    db.commit()

    ts = datetime.now().isoformat(timespec='seconds')
    print(f'[{ts}] Starte Messung...')

    d = {}

    # Live / Echtzeit
    set_field(d, 'battery_power_w',      i16(30001))
    set_field(d, 'ac_power_w',           i16(30006))
    set_field(d, 'battery_voltage_v',    sc(u16(30100), 0.01))
    set_field(d, 'battery_current_a',    sc(i16(30101), 0.1))
    set_field(d, 'battery_soc_pct',      u16(32104))
    set_field(d, 'battery_capacity_kwh', sc(u16(32105), 0.001))
    set_field(d, 'inverter_state',       u16(35100))

    # AC / Netz
    set_field(d, 'ac_voltage_v',         sc(u16(32200), 0.1))
    set_field(d, 'ac_current_a',         sc(i16(37004), 0.004))
    set_field(d, 'ac_frequency_hz',      sc(i16(32204), 0.1))

    # Temperatur
    set_field(d, 'internal_temp_c',      sc(i16(35000), 0.1))
    set_field(d, 'mos1_temp_c',          sc(i16(35001), 0.1))
    set_field(d, 'mos2_temp_c',          sc(i16(35002), 0.1))
    set_field(d, 'max_cell_temp_c',      sc(i16(35010), 0.1))
    set_field(d, 'min_cell_temp_c',      sc(i16(35011), 0.1))
    set_field(d, 'max_cell_voltage_v',   sc(i16(37007), 0.001))
    set_field(d, 'min_cell_voltage_v',   sc(i16(37008), 0.001))

    # Energie-Zaehler (32-bit)
    set_field(d, 'total_charge_kwh',      sc(u32(33000), 0.01))
    set_field(d, 'total_discharge_kwh',   sc(i32(33002), 0.01))
    set_field(d, 'daily_charge_kwh',      sc(u32(33004), 0.01))
    set_field(d, 'daily_discharge_kwh',   sc(i32(33006), 0.01))
    set_field(d, 'monthly_charge_kwh',    sc(u32(33008), 0.01))
    set_field(d, 'monthly_discharge_kwh', sc(i32(33010), 0.01))
    set_field(d, 'cycle_count',           u16(34003))

    # Zellspannungen Modul 1
    for i in range(13):
        set_field(d, f'cell{i+1}_v', sc(i16(34018 + i), 0.001))

    # MPPT Solar
    for n, (rv, ra, rw) in enumerate(
        [(30020,30024,30037),(30021,30025,30038),(30022,30026,30039),(30023,30027,30040)], 1
    ):
        set_field(d, f'mppt{n}_voltage_v', sc(u16(rv), 0.1))
        set_field(d, f'mppt{n}_current_a', sc(u16(ra), 0.1))
        set_field(d, f'mppt{n}_power_w',   sc(u16(rw), 0.1))

    # Diagnose / Konfiguration
    raw_wifi = u16(30303)
    set_field(d, 'wifi_signal_dbm', None if raw_wifi is None else raw_wifi * -1)
    set_field(d, 'work_mode',       u16(43000))
    set_field(d, 'force_mode',      u16(42010))
    set_field(d, 'max_charge_w',    u16(44002))
    set_field(d, 'max_discharge_w', u16(44003))

    cols = ', '.join(['ts'] + list(d.keys()))
    plh  = ', '.join(['?'] * (1 + len(d)))
    db.execute(f'INSERT INTO readings ({cols}) VALUES ({plh})', [ts] + list(d.values()))
    db.commit()
    db.close()

    print(f'  OK - SOC: {d.get("battery_soc_pct")} %  '
          f'Akku: {d.get("battery_power_w")} W  '
          f'Zyklen: {d.get("cycle_count")}')

if __name__ == '__main__':
    try:
        collect()
    except Exception as e:
        print(f'FEHLER: {e}', flush=True)
        os._exit(1)
    sys.stdout.flush()
    os._exit(0)   # Python-Cleanup ueberspringen (verhindert Haenger durch Subprocess-Cleanup)
