add daemon mode
This commit is contained in:
parent
10d00c8fbb
commit
814c41098f
|
|
@ -21,3 +21,5 @@ htmlcov/
|
||||||
# Editors / OS
|
# Editors / OS
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
importtime.txt
|
||||||
|
hopi_readings.csv
|
||||||
|
|
|
||||||
39
Hopi.py
39
Hopi.py
|
|
@ -1,4 +1,6 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from hopi.data_handler import PowerMeterDataHandler
|
from hopi.data_handler import PowerMeterDataHandler
|
||||||
from hopi.serial_client import ModbusRtuSerialClient, SerialConfig
|
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("--port", default="/dev/ttyUSB0")
|
||||||
parser.add_argument("--baud", type=int, default=9600)
|
parser.add_argument("--baud", type=int, default=9600)
|
||||||
parser.add_argument("--timeout", type=float, default=0.1)
|
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
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,6 +30,39 @@ def main() -> None:
|
||||||
client = ModbusRtuSerialClient(config)
|
client = ModbusRtuSerialClient(config)
|
||||||
handler = PowerMeterDataHandler()
|
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:
|
with client:
|
||||||
response = client.read_holding_registers(
|
response = client.read_holding_registers(
|
||||||
slave_id=SLAVE_ID,
|
slave_id=SLAVE_ID,
|
||||||
|
|
@ -36,7 +74,6 @@ def main() -> None:
|
||||||
|
|
||||||
registers = handler.parse_read_holding_registers_response(response)
|
registers = handler.parse_read_holding_registers_response(response)
|
||||||
readings = handler.decode_readings(registers)
|
readings = handler.decode_readings(registers)
|
||||||
|
|
||||||
handler.print_readings_json(readings)
|
handler.print_readings_json(readings)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,45 @@
|
||||||
|
import csv
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
from typing import Iterable, List
|
from typing import List, Optional
|
||||||
|
|
||||||
from .models import PowerMeterReadings
|
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:
|
class PowerMeterDataHandler:
|
||||||
"""Parses Modbus RTU responses and computes derived power metrics."""
|
"""Parses Modbus RTU responses and computes derived power metrics."""
|
||||||
|
|
||||||
|
|
@ -90,6 +124,31 @@ class PowerMeterDataHandler:
|
||||||
def print_readings_json(self, readings: PowerMeterReadings) -> None:
|
def print_readings_json(self, readings: PowerMeterReadings) -> None:
|
||||||
print(self.readings_to_json(readings, ensure_ascii=False))
|
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
|
@staticmethod
|
||||||
def _bytes_to_registers_be(data: bytes) -> List[int]:
|
def _bytes_to_registers_be(data: bytes) -> List[int]:
|
||||||
registers: 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