scheint zu klappen.
This commit is contained in:
@ -5,7 +5,7 @@
|
|||||||
"read_timeout_sec": 25,
|
"read_timeout_sec": 25,
|
||||||
"connect_timeout_sec": 10,
|
"connect_timeout_sec": 10,
|
||||||
"command": "ostatus",
|
"command": "ostatus",
|
||||||
"prompt_timeout_sec": 25
|
"prompt_timeout_sec": 8
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"write_markdown": true,
|
"write_markdown": true,
|
||||||
|
|||||||
BIN
data/mmp.sqlite
Normal file
BIN
data/mmp.sqlite
Normal file
Binary file not shown.
164
mmp_logger.py
164
mmp_logger.py
@ -1,21 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
mmp_logger.py
|
||||||
|
- Pollt BayTech MMP (seriell->TCP bridge, raw TCP) per Kommando "ostatus"
|
||||||
|
- Schreibt Messwerte in SQLite
|
||||||
|
- Fehlende Felder werden mit letztem Messwert ersetzt (filled + filled_fields werden protokolliert)
|
||||||
|
- Report als Markdown (und optional HTML/PDF) nach ./reports/
|
||||||
|
|
||||||
|
Projektidee: Alles in EINEM Verzeichnis.
|
||||||
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
|
||||||
import socket
|
import socket
|
||||||
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
# Prompt endet mit '>' (laut deiner Beschreibung)
|
# -------------------------
|
||||||
|
# Parsing / Prompt
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
PROMPT_END = b">"
|
PROMPT_END = b">"
|
||||||
NUM_RE = re.compile(r"([-+]?\d+(\.\d+)?)")
|
NUM_RE = re.compile(r"([-+]?\d+(\.\d+)?)")
|
||||||
|
FILL_KEYS = ["current_a", "peak_a", "voltage_v", "power_w", "va", "state"]
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Data classes
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeviceCfg:
|
class DeviceCfg:
|
||||||
@ -23,7 +42,7 @@ class DeviceCfg:
|
|||||||
host: str
|
host: str
|
||||||
port: int
|
port: int
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None # nicht genutzt (kein Login), bleibt für Zukunft
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +130,7 @@ def db_init(con: sqlite3.Connection) -> None:
|
|||||||
poll_run_id INTEGER NOT NULL,
|
poll_run_id INTEGER NOT NULL,
|
||||||
device_id INTEGER NOT NULL,
|
device_id INTEGER NOT NULL,
|
||||||
outlet_id INTEGER NOT NULL,
|
outlet_id INTEGER NOT NULL,
|
||||||
ts TEXT NOT NULL, -- UTC ISO
|
ts TEXT NOT NULL,
|
||||||
current_a REAL,
|
current_a REAL,
|
||||||
peak_a REAL,
|
peak_a REAL,
|
||||||
voltage_v REAL,
|
voltage_v REAL,
|
||||||
@ -135,11 +154,11 @@ def get_or_create_device(con: sqlite3.Connection, d: DeviceCfg) -> int:
|
|||||||
cur = con.execute("SELECT id FROM device WHERE name=?", (d.name,))
|
cur = con.execute("SELECT id FROM device WHERE name=?", (d.name,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if row:
|
if row:
|
||||||
device_id = row[0]
|
device_id = int(row[0])
|
||||||
con.execute("UPDATE device SET host=?, port=?, last_seen_at=? WHERE id=?",
|
con.execute("UPDATE device SET host=?, port=?, last_seen_at=? WHERE id=?",
|
||||||
(d.host, d.port, now, device_id))
|
(d.host, d.port, now, device_id))
|
||||||
con.commit()
|
con.commit()
|
||||||
return int(device_id)
|
return device_id
|
||||||
|
|
||||||
cur = con.execute(
|
cur = con.execute(
|
||||||
"INSERT INTO device(name,host,port,created_at,last_seen_at) VALUES(?,?,?,?,?)",
|
"INSERT INTO device(name,host,port,created_at,last_seen_at) VALUES(?,?,?,?,?)",
|
||||||
@ -204,17 +223,15 @@ def cost_center_for(outlet_name: str, cc_map: Dict[str, str]) -> Tuple[str, str]
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Telnet / Fetch / Parse
|
# Raw TCP (seriell->TCP bridge)
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
PROMPT_END = b">"
|
def tcp_read_all(sock: socket.socket, read_timeout: int, stop_on_prompt: bool = True) -> bytes:
|
||||||
|
|
||||||
def tcp_read_all(sock: socket.socket, read_timeout: int) -> bytes:
|
|
||||||
"""
|
"""
|
||||||
Liest bis:
|
Liest Daten bis:
|
||||||
- Prompt '>' erkannt wird, ODER
|
- prompt '>' gesehen (optional)
|
||||||
- Gegenseite schließt (recv == b''), ODER
|
- remote close (recv == b'')
|
||||||
- read_timeout abläuft (socket.timeout)
|
- timeout
|
||||||
"""
|
"""
|
||||||
sock.settimeout(read_timeout)
|
sock.settimeout(read_timeout)
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
@ -224,11 +241,9 @@ def tcp_read_all(sock: socket.socket, read_timeout: int) -> bytes:
|
|||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
break
|
break
|
||||||
if not chunk:
|
if not chunk:
|
||||||
# Session geschlossen
|
|
||||||
break
|
break
|
||||||
buf += chunk
|
buf += chunk
|
||||||
# Prompt irgendwo im Buffer?
|
if stop_on_prompt and (PROMPT_END in buf):
|
||||||
if PROMPT_END in buf:
|
|
||||||
break
|
break
|
||||||
return bytes(buf)
|
return bytes(buf)
|
||||||
|
|
||||||
@ -242,46 +257,49 @@ def tcp_fetch_ostatus_raw(
|
|||||||
enter_first: bool = True,
|
enter_first: bool = True,
|
||||||
prompt_pause_sec: float = 2.0,
|
prompt_pause_sec: float = 2.0,
|
||||||
cmd_pause_sec: float = 0.2,
|
cmd_pause_sec: float = 0.2,
|
||||||
|
stop_on_prompt: bool = True
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Raw TCP (seriell->TCP). Ablauf:
|
Ablauf (weil Session ggf. schließt):
|
||||||
1) connect
|
1) connect
|
||||||
2) optional: ENTER senden, kurz warten
|
2) ENTER senden (wecken), Pause
|
||||||
3) kurz lesen (prompt_timeout)
|
3) kurz lesen (prompt_timeout) -> wenn kein Prompt: nochmal ENTER+Pause+kurz lesen
|
||||||
- falls kein '>' kommt: nochmal ENTER, Pause
|
|
||||||
4) cmd senden, kurze Pause
|
4) cmd senden, kurze Pause
|
||||||
5) lesen bis prompt / close / timeout
|
5) lesen bis prompt/close/timeout
|
||||||
"""
|
"""
|
||||||
with socket.create_connection((host, port), timeout=connect_timeout) as s:
|
with socket.create_connection((host, port), timeout=connect_timeout) as s:
|
||||||
# 1) "Session wecken"
|
pre = b""
|
||||||
if enter_first:
|
if enter_first:
|
||||||
s.sendall(b"\r\n")
|
s.sendall(b"\r\n")
|
||||||
time.sleep(prompt_pause_sec)
|
time.sleep(prompt_pause_sec)
|
||||||
|
|
||||||
# 2) kurzer Versuch: gibt's schon Output/Prompt?
|
pre += tcp_read_all(s, read_timeout=prompt_timeout, stop_on_prompt=stop_on_prompt)
|
||||||
pre = tcp_read_all(s, read_timeout=prompt_timeout)
|
|
||||||
|
|
||||||
# 3) falls kein Prompt: nochmal Enter + Pause
|
if stop_on_prompt and (PROMPT_END not in pre):
|
||||||
if PROMPT_END not in pre:
|
# nochmal wecken
|
||||||
s.sendall(b"\r\n")
|
s.sendall(b"\r\n")
|
||||||
time.sleep(prompt_pause_sec)
|
time.sleep(prompt_pause_sec)
|
||||||
pre += tcp_read_all(s, read_timeout=prompt_timeout)
|
pre += tcp_read_all(s, read_timeout=prompt_timeout, stop_on_prompt=stop_on_prompt)
|
||||||
|
|
||||||
# 4) Kommando senden
|
# Kommando
|
||||||
s.sendall(cmd.encode("utf-8") + b"\r\n")
|
s.sendall(cmd.encode("utf-8") + b"\r\n")
|
||||||
time.sleep(cmd_pause_sec)
|
time.sleep(cmd_pause_sec)
|
||||||
|
|
||||||
# 5) Antwort lesen (Bridge kann danach schließen)
|
out = tcp_read_all(s, read_timeout=read_timeout, stop_on_prompt=stop_on_prompt)
|
||||||
out = tcp_read_all(s, read_timeout=read_timeout)
|
|
||||||
return pre + out
|
return pre + out
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Parse ostatus output
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
def parse_ostatus(text: str) -> List[dict]:
|
def parse_ostatus(text: str) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Parst Tabellenzeilen mit '|' (dein Format).
|
Erwartet Tabellenzeilen mit '|'.
|
||||||
Beispielzeile:
|
Beispiel:
|
||||||
| W Power1 | 0.0 A | 0.0 A | 230.3 V | 0 W | 4 VA | On |
|
| W Power1 | 0.0 A | 0.0 A | 230.3 V | 0 W | 4 VA | On |
|
||||||
"""
|
"""
|
||||||
rows = []
|
rows: List[dict] = []
|
||||||
for ln in text.splitlines():
|
for ln in text.splitlines():
|
||||||
if "|" not in ln:
|
if "|" not in ln:
|
||||||
continue
|
continue
|
||||||
@ -319,8 +337,6 @@ def parse_ostatus(text: str) -> List[dict]:
|
|||||||
# Fill logic
|
# Fill logic
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
FILL_KEYS = ["current_a", "peak_a", "voltage_v", "power_w", "va", "state"]
|
|
||||||
|
|
||||||
def apply_fill(row: dict, last: Optional[dict]) -> Tuple[dict, int, int]:
|
def apply_fill(row: dict, last: Optional[dict]) -> Tuple[dict, int, int]:
|
||||||
if not last:
|
if not last:
|
||||||
return row, 0, 0
|
return row, 0, 0
|
||||||
@ -338,7 +354,11 @@ def apply_fill(row: dict, last: Optional[dict]) -> Tuple[dict, int, int]:
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
def poll_device(con: sqlite3.Connection, dev: DeviceCfg, cc_map: Dict[str, str],
|
def poll_device(con: sqlite3.Connection, dev: DeviceCfg, cc_map: Dict[str, str],
|
||||||
cmd: str, connect_timeout: int, prompt_timeout: int, read_timeout: int) -> None:
|
cmd: str, connect_timeout: int, prompt_timeout: int, read_timeout: int,
|
||||||
|
enter_first: bool, prompt_pause_sec: float,
|
||||||
|
stop_on_prompt: bool,
|
||||||
|
debug_dump_raw: bool,
|
||||||
|
project_root: str) -> None:
|
||||||
device_id = get_or_create_device(con, dev)
|
device_id = get_or_create_device(con, dev)
|
||||||
started = utc_now_iso()
|
started = utc_now_iso()
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@ -361,10 +381,22 @@ def poll_device(con: sqlite3.Connection, dev: DeviceCfg, cc_map: Dict[str, str],
|
|||||||
connect_timeout=connect_timeout,
|
connect_timeout=connect_timeout,
|
||||||
prompt_timeout=prompt_timeout,
|
prompt_timeout=prompt_timeout,
|
||||||
read_timeout=read_timeout,
|
read_timeout=read_timeout,
|
||||||
enter_first=True,
|
enter_first=enter_first,
|
||||||
prompt_pause_sec=2.0
|
prompt_pause_sec=prompt_pause_sec,
|
||||||
|
stop_on_prompt=stop_on_prompt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
text = raw.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
if debug_dump_raw:
|
||||||
|
logs_dir = Path(project_root) / "logs"
|
||||||
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(logs_dir / f"raw_{dev.name}.txt").write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
rows = parse_ostatus(text)
|
||||||
|
ts = utc_now_iso()
|
||||||
|
outlets_received = len(rows)
|
||||||
|
|
||||||
for r in rows:
|
for r in rows:
|
||||||
outlet_name = r["outlet_name"].strip()
|
outlet_name = r["outlet_name"].strip()
|
||||||
cost_code, cost_name = cost_center_for(outlet_name, cc_map)
|
cost_code, cost_name = cost_center_for(outlet_name, cc_map)
|
||||||
@ -400,6 +432,9 @@ def poll_device(con: sqlite3.Connection, dev: DeviceCfg, cc_map: Dict[str, str],
|
|||||||
""", (utc_now_iso(), outlets_received, outlets_filled, fields_filled, duration_ms, run_id))
|
""", (utc_now_iso(), outlets_received, outlets_filled, fields_filled, duration_ms, run_id))
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
|
# kleine Info für CLI
|
||||||
|
print(f"{dev.name}: OK outlets={outlets_received} filled_outlets={outlets_filled} filled_fields={fields_filled}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
duration_ms = int((time.time() - t0) * 1000)
|
duration_ms = int((time.time() - t0) * 1000)
|
||||||
con.execute("""
|
con.execute("""
|
||||||
@ -473,7 +508,6 @@ def parse_period_args(args) -> Tuple[str, str, str]:
|
|||||||
return start.isoformat(), now.isoformat(), f"last{args.last_days}d"
|
return start.isoformat(), now.isoformat(), f"last{args.last_days}d"
|
||||||
|
|
||||||
if args.from_iso and args.to_iso:
|
if args.from_iso and args.to_iso:
|
||||||
# Name aus from/to (gekürzt)
|
|
||||||
return args.from_iso, args.to_iso, "custom"
|
return args.from_iso, args.to_iso, "custom"
|
||||||
|
|
||||||
if args.period == "weekly":
|
if args.period == "weekly":
|
||||||
@ -493,8 +527,8 @@ def parse_period_args(args) -> Tuple[str, str, str]:
|
|||||||
|
|
||||||
def report(con: sqlite3.Connection, from_iso: str, to_iso: str, device_name: Optional[str]) -> str:
|
def report(con: sqlite3.Connection, from_iso: str, to_iso: str, device_name: Optional[str]) -> str:
|
||||||
"""
|
"""
|
||||||
Erzeugt Markdown-Report als String (kann gedruckt + in Dateien geschrieben werden).
|
Markdown Report als String.
|
||||||
Energie: grob approx_Wh = Sum(power_w) * 1h (bei stündlichem Poll).
|
Energie: approx_Wh = Sum(power_w)*1h (bei stündlichem Poll).
|
||||||
"""
|
"""
|
||||||
params = [from_iso, to_iso]
|
params = [from_iso, to_iso]
|
||||||
dev_filter = ""
|
dev_filter = ""
|
||||||
@ -540,7 +574,7 @@ def report(con: sqlite3.Connection, from_iso: str, to_iso: str, device_name: Opt
|
|||||||
"""
|
"""
|
||||||
job_rows = con.execute(q_job, params).fetchall()
|
job_rows = con.execute(q_job, params).fetchall()
|
||||||
|
|
||||||
md = []
|
md: List[str] = []
|
||||||
md.append("# MMP Report")
|
md.append("# MMP Report")
|
||||||
md.append("")
|
md.append("")
|
||||||
md.append(f"- Zeitraum (UTC): **{from_iso}** .. **{to_iso}**")
|
md.append(f"- Zeitraum (UTC): **{from_iso}** .. **{to_iso}**")
|
||||||
@ -572,7 +606,7 @@ def report(con: sqlite3.Connection, from_iso: str, to_iso: str, device_name: Opt
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description="MMP ostatus Logger -> SQLite + Reports (md/html/pdf)")
|
ap = argparse.ArgumentParser(description="MMP ostatus Logger (raw TCP) -> SQLite + Reports (md/html/pdf)")
|
||||||
ap.add_argument("--config", required=True, help="Pfad zur config.json")
|
ap.add_argument("--config", required=True, help="Pfad zur config.json")
|
||||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
@ -591,31 +625,39 @@ def main():
|
|||||||
cfg = load_json(args.config)
|
cfg = load_json(args.config)
|
||||||
proj_root = project_root_from_config(args.config)
|
proj_root = project_root_from_config(args.config)
|
||||||
|
|
||||||
# Projekt-Unterordner sicherstellen (alles im selben Verzeichnis)
|
# Projekt-Unterordner sicherstellen
|
||||||
safe_mkdir(proj_root, "data")
|
safe_mkdir(proj_root, "data")
|
||||||
safe_mkdir(proj_root, "logs")
|
safe_mkdir(proj_root, "logs")
|
||||||
safe_mkdir(proj_root, "reports")
|
safe_mkdir(proj_root, "reports")
|
||||||
|
|
||||||
|
# Pfade relativ zur config.json
|
||||||
db_path = resolve_path(args.config, cfg["db_path"])
|
db_path = resolve_path(args.config, cfg["db_path"])
|
||||||
cc_path = resolve_path(args.config, cfg["cost_center_map"])
|
cc_path = resolve_path(args.config, cfg["cost_center_map"])
|
||||||
|
|
||||||
cc_map = load_json(cc_path)
|
cc_map = load_json(cc_path)
|
||||||
|
|
||||||
con = db_connect(db_path)
|
con = db_connect(db_path)
|
||||||
db_init(con)
|
db_init(con)
|
||||||
|
|
||||||
tel = cfg.get("telnet", {})
|
# TCP / Timeouts / Verhalten
|
||||||
read_timeout = int(tel.get("read_timeout_sec", 25))
|
tcp_cfg = cfg.get("tcp", cfg.get("telnet", {})) # erlaubt "telnet" alt, oder "tcp" neu
|
||||||
connect_timeout = int(tel.get("connect_timeout_sec", 10))
|
read_timeout = int(tcp_cfg.get("read_timeout_sec", 35))
|
||||||
prompt_timeout = int(tel.get("prompt_timeout_sec", 25))
|
connect_timeout = int(tcp_cfg.get("connect_timeout_sec", 10))
|
||||||
cmd = tel.get("command", "ostatus")
|
prompt_timeout = int(tcp_cfg.get("prompt_timeout_sec", 8))
|
||||||
|
cmd = tcp_cfg.get("command", "ostatus")
|
||||||
|
|
||||||
|
enter_first = bool(tcp_cfg.get("enter_first", True))
|
||||||
|
prompt_pause_sec = float(tcp_cfg.get("prompt_pause_sec", 2.0))
|
||||||
|
stop_on_prompt = bool(tcp_cfg.get("stop_on_prompt", True))
|
||||||
|
debug_dump_raw = bool(tcp_cfg.get("debug_dump_raw", False))
|
||||||
|
|
||||||
|
# Report-Ausgabe
|
||||||
rep_cfg = cfg.get("report", {})
|
rep_cfg = cfg.get("report", {})
|
||||||
write_md = bool(rep_cfg.get("write_markdown", True))
|
write_md = bool(rep_cfg.get("write_markdown", True))
|
||||||
write_html = bool(rep_cfg.get("write_html", True))
|
write_html = bool(rep_cfg.get("write_html", True))
|
||||||
write_pdf = bool(rep_cfg.get("write_pdf", False))
|
write_pdf = bool(rep_cfg.get("write_pdf", False))
|
||||||
name_prefix = rep_cfg.get("report_name_prefix", "report")
|
name_prefix = rep_cfg.get("report_name_prefix", "report")
|
||||||
|
|
||||||
|
# Devices
|
||||||
devices: List[DeviceCfg] = []
|
devices: List[DeviceCfg] = []
|
||||||
for d in cfg.get("devices", []):
|
for d in cfg.get("devices", []):
|
||||||
devices.append(DeviceCfg(
|
devices.append(DeviceCfg(
|
||||||
@ -628,13 +670,24 @@ def main():
|
|||||||
))
|
))
|
||||||
|
|
||||||
if args.cmd == "poll":
|
if args.cmd == "poll":
|
||||||
|
any_ran = False
|
||||||
for d in devices:
|
for d in devices:
|
||||||
if not d.enabled:
|
if not d.enabled:
|
||||||
continue
|
continue
|
||||||
if args.device and d.name != args.device:
|
if args.device and d.name != args.device:
|
||||||
continue
|
continue
|
||||||
poll_device(con, d, cc_map, cmd, connect_timeout, prompt_timeout, read_timeout)
|
any_ran = True
|
||||||
print("OK")
|
poll_device(
|
||||||
|
con, d, cc_map,
|
||||||
|
cmd, connect_timeout, prompt_timeout, read_timeout,
|
||||||
|
enter_first=enter_first,
|
||||||
|
prompt_pause_sec=prompt_pause_sec,
|
||||||
|
stop_on_prompt=stop_on_prompt,
|
||||||
|
debug_dump_raw=debug_dump_raw,
|
||||||
|
project_root=proj_root
|
||||||
|
)
|
||||||
|
if not any_ran:
|
||||||
|
print("Hinweis: Kein enabled device gematcht (check config.json / --device).")
|
||||||
|
|
||||||
elif args.cmd == "report":
|
elif args.cmd == "report":
|
||||||
from_iso, to_iso, suffix = parse_period_args(args)
|
from_iso, to_iso, suffix = parse_period_args(args)
|
||||||
@ -642,7 +695,6 @@ def main():
|
|||||||
|
|
||||||
print(md_text)
|
print(md_text)
|
||||||
|
|
||||||
# Dateiname: z.B. report_weekly, report_monthly, report_last14d, report_custom
|
|
||||||
name = f"{name_prefix}_{suffix}"
|
name = f"{name_prefix}_{suffix}"
|
||||||
if args.device:
|
if args.device:
|
||||||
name = f"{name}_{args.device}"
|
name = f"{name}_{args.device}"
|
||||||
@ -656,6 +708,6 @@ def main():
|
|||||||
write_pdf=write_pdf
|
write_pdf=write_pdf
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
Reference in New Issue
Block a user