add daemon mode
This commit is contained in:
parent
10d00c8fbb
commit
814c41098f
|
|
@ -21,3 +21,5 @@ htmlcov/
|
|||
# Editors / OS
|
||||
.vscode/
|
||||
.DS_Store
|
||||
importtime.txt
|
||||
hopi_readings.csv
|
||||
|
|
|
|||
39
Hopi.py
39
Hopi.py
|
|
@ -1,4 +1,6 @@
|
|||
import argparse
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from hopi.data_handler import PowerMeterDataHandler
|
||||
from hopi.serial_client import ModbusRtuSerialClient, SerialConfig
|
||||
|
|
@ -15,6 +17,9 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|||
parser.add_argument("--port", default="/dev/ttyUSB0")
|
||||
parser.add_argument("--baud", type=int, default=9600)
|
||||
parser.add_argument("--timeout", type=float, default=0.1)
|
||||
parser.add_argument("--daemon", action="store_true", help="Run continuously until stopped (Ctrl+C)")
|
||||
parser.add_argument("--csv", default="hopi_readings.csv", help="CSV output path for daemon mode")
|
||||
parser.add_argument("--interval", type=float, default=1.0, help="Seconds between reads in daemon mode")
|
||||
return parser
|
||||
|
||||
|
||||
|
|
@ -25,6 +30,39 @@ def main() -> None:
|
|||
client = ModbusRtuSerialClient(config)
|
||||
handler = PowerMeterDataHandler()
|
||||
|
||||
if args.daemon:
|
||||
if args.timeout <= 0:
|
||||
raise ValueError("--timeout must be > 0")
|
||||
if args.interval <= 0:
|
||||
raise ValueError("--interval must be > 0")
|
||||
if args.interval <= args.timeout:
|
||||
raise ValueError("--interval must be > --timeout")
|
||||
|
||||
next_deadline = time.monotonic()
|
||||
with client, handler.open_csv_logger(args.csv) as logger:
|
||||
try:
|
||||
while True:
|
||||
response = client.read_holding_registers(
|
||||
slave_id=SLAVE_ID,
|
||||
start_register=START_REG,
|
||||
register_count=NUM_WORDS,
|
||||
read_max_bytes=READ_MAX_BYTES,
|
||||
exact_response_length=True,
|
||||
)
|
||||
registers = handler.parse_read_holding_registers_response(response)
|
||||
readings = handler.decode_readings(registers)
|
||||
|
||||
row = {"timestamp": datetime.now(timezone.utc).isoformat()}
|
||||
row.update(handler.readings_to_flat_dict(readings))
|
||||
logger.log(row)
|
||||
|
||||
next_deadline += args.interval
|
||||
sleep_for = next_deadline - time.monotonic()
|
||||
if sleep_for > 0:
|
||||
time.sleep(sleep_for)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
with client:
|
||||
response = client.read_holding_registers(
|
||||
slave_id=SLAVE_ID,
|
||||
|
|
@ -36,7 +74,6 @@ def main() -> None:
|
|||
|
||||
registers = handler.parse_read_holding_registers_response(response)
|
||||
readings = handler.decode_readings(registers)
|
||||
|
||||
handler.print_readings_json(readings)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,45 @@
|
|||
import csv
|
||||
import json
|
||||
import os
|
||||
import math
|
||||
import struct
|
||||
from typing import Iterable, List
|
||||
from typing import List, Optional
|
||||
|
||||
from .models import PowerMeterReadings
|
||||
|
||||
|
||||
class ReadingsCsvLogger:
|
||||
def __init__(self, file_path: str, fieldnames: List[str]):
|
||||
self._file_path = file_path
|
||||
self._fieldnames = fieldnames
|
||||
self._fp = None
|
||||
self._writer: Optional[csv.DictWriter] = None
|
||||
|
||||
def __enter__(self) -> "ReadingsCsvLogger":
|
||||
is_new_file = (not os.path.exists(self._file_path)) or os.path.getsize(self._file_path) == 0
|
||||
self._fp = open(self._file_path, "a", newline="", encoding="utf-8")
|
||||
self._writer = csv.DictWriter(self._fp, fieldnames=self._fieldnames)
|
||||
if is_new_file:
|
||||
self._writer.writeheader()
|
||||
self._fp.flush()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self._fp is not None:
|
||||
try:
|
||||
self._fp.flush()
|
||||
finally:
|
||||
self._fp.close()
|
||||
self._fp = None
|
||||
self._writer = None
|
||||
|
||||
def log(self, row: dict) -> None:
|
||||
if self._writer is None or self._fp is None:
|
||||
raise RuntimeError("CSV logger is not open")
|
||||
self._writer.writerow(row)
|
||||
self._fp.flush()
|
||||
|
||||
|
||||
class PowerMeterDataHandler:
|
||||
"""Parses Modbus RTU responses and computes derived power metrics."""
|
||||
|
||||
|
|
@ -90,6 +124,31 @@ class PowerMeterDataHandler:
|
|||
def print_readings_json(self, readings: PowerMeterReadings) -> None:
|
||||
print(self.readings_to_json(readings, ensure_ascii=False))
|
||||
|
||||
def open_csv_logger(self, file_path: str) -> ReadingsCsvLogger:
|
||||
fieldnames = ["timestamp"] + self._flat_readings_keys()
|
||||
return ReadingsCsvLogger(file_path=file_path, fieldnames=fieldnames)
|
||||
|
||||
@staticmethod
|
||||
def _flat_readings_keys() -> List[str]:
|
||||
# Keep order stable for CSV columns.
|
||||
return [
|
||||
"active_power_w",
|
||||
"rms_current_a",
|
||||
"voltage_v",
|
||||
"frequency_hz",
|
||||
"power_factor",
|
||||
"annual_power_consumption_kwh",
|
||||
"active_consumption_kwh",
|
||||
"reactive_consumption_kwh",
|
||||
"load_time_hours",
|
||||
"work_hours_per_day",
|
||||
"device_address",
|
||||
"apparent_power_vi_va",
|
||||
"apparent_power_pf_va",
|
||||
"reactive_power_var",
|
||||
"apparent_consumption_kvah",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _bytes_to_registers_be(data: bytes) -> List[int]:
|
||||
registers: List[int] = []
|
||||
|
|
|
|||
101
importtime.txt
101
importtime.txt
|
|
@ -1,101 +0,0 @@
|
|||
import time: self [us] | cumulative | imported package
|
||||
import time: 268 | 268 | _io
|
||||
import time: 54 | 54 | marshal
|
||||
import time: 407 | 407 | posix
|
||||
import time: 892 | 1619 | _frozen_importlib_external
|
||||
import time: 92 | 92 | time
|
||||
import time: 298 | 389 | zipimport
|
||||
import time: 51 | 51 | _codecs
|
||||
import time: 532 | 582 | codecs
|
||||
import time: 436 | 436 | encodings.aliases
|
||||
import time: 582 | 1599 | encodings
|
||||
import time: 199 | 199 | encodings.utf_8
|
||||
import time: 91 | 91 | _signal
|
||||
import time: 36 | 36 | _abc
|
||||
import time: 167 | 202 | abc
|
||||
import time: 163 | 365 | io
|
||||
import time: 44 | 44 | _stat
|
||||
import time: 107 | 150 | stat
|
||||
import time: 929 | 929 | _collections_abc
|
||||
import time: 60 | 60 | errno
|
||||
import time: 93 | 93 | genericpath
|
||||
import time: 159 | 311 | posixpath
|
||||
import time: 524 | 1912 | os
|
||||
import time: 109 | 109 | _sitebuiltins
|
||||
import time: 427 | 427 | encodings.utf_8_sig
|
||||
import time: 642 | 642 | _distutils_hack
|
||||
import time: 306 | 306 | types
|
||||
import time: 152 | 152 | importlib
|
||||
import time: 184 | 184 | importlib._abc
|
||||
import time: 199 | 534 | importlib.util
|
||||
import time: 62 | 62 | importlib.machinery
|
||||
import time: 60 | 60 | sitecustomize
|
||||
import time: 51 | 51 | usercustomize
|
||||
import time: 1437 | 5537 | site
|
||||
import time: 345 | 345 | itertools
|
||||
import time: 137 | 137 | keyword
|
||||
import time: 85 | 85 | _operator
|
||||
import time: 284 | 369 | operator
|
||||
import time: 227 | 227 | reprlib
|
||||
import time: 75 | 75 | _collections
|
||||
import time: 833 | 1984 | collections
|
||||
import time: 79 | 79 | _functools
|
||||
import time: 650 | 2712 | functools
|
||||
import time: 1875 | 4587 | enum
|
||||
import time: 113 | 113 | _sre
|
||||
import time: 309 | 309 | re._constants
|
||||
import time: 503 | 811 | re._parser
|
||||
import time: 123 | 123 | re._casefix
|
||||
import time: 960 | 2006 | re._compiler
|
||||
import time: 186 | 186 | copyreg
|
||||
import time: 632 | 7409 | re
|
||||
import time: 1521 | 1521 | gettext
|
||||
import time: 1146 | 10075 | argparse
|
||||
import time: 114 | 114 | hopi
|
||||
import time: 213 | 213 | _json
|
||||
import time: 355 | 567 | json.scanner
|
||||
import time: 467 | 1034 | json.decoder
|
||||
import time: 390 | 390 | json.encoder
|
||||
import time: 210 | 1633 | json
|
||||
import time: 208 | 208 | math
|
||||
import time: 169 | 169 | _struct
|
||||
import time: 144 | 313 | struct
|
||||
import time: 50 | 50 | _typing
|
||||
import time: 2816 | 2865 | typing
|
||||
import time: 235 | 235 | _weakrefset
|
||||
import time: 446 | 680 | weakref
|
||||
import time: 210 | 890 | copy
|
||||
import time: 1398 | 1398 | _ast
|
||||
import time: 609 | 609 | contextlib
|
||||
import time: 1228 | 3233 | ast
|
||||
import time: 211 | 211 | _opcode
|
||||
import time: 231 | 231 | _opcode_metadata
|
||||
import time: 309 | 750 | opcode
|
||||
import time: 908 | 1657 | dis
|
||||
import time: 167 | 167 | linecache
|
||||
import time: 180 | 180 | token
|
||||
import time: 42 | 42 | _tokenize
|
||||
import time: 815 | 1037 | tokenize
|
||||
import time: 2370 | 8462 | inspect
|
||||
import time: 716 | 10066 | dataclasses
|
||||
import time: 1404 | 11470 | hopi.models
|
||||
import time: 401 | 17002 | hopi.data_handler
|
||||
import time: 144 | 144 | __future__
|
||||
import time: 438 | 438 | serial.serialutil
|
||||
import time: 168 | 168 | fcntl
|
||||
import time: 165 | 165 | select
|
||||
import time: 216 | 216 | termios
|
||||
import time: 180 | 180 | array
|
||||
import time: 488 | 1215 | serial.serialposix
|
||||
import time: 215 | 2011 | serial
|
||||
import time: 951 | 2962 | hopi.serial_client
|
||||
import time: 81 | 81 | _locale
|
||||
import time: 909 | 990 | locale
|
||||
import time: 170 | 170 | fnmatch
|
||||
import time: 343 | 343 | zlib
|
||||
import time: 211 | 211 | _compression
|
||||
import time: 226 | 226 | _bz2
|
||||
import time: 271 | 708 | bz2
|
||||
import time: 265 | 265 | _lzma
|
||||
import time: 266 | 531 | lzma
|
||||
import time: 951 | 2700 | shutil
|
||||
Loading…
Reference in New Issue