Add new daemons and debug scripts for Sigenergy and Oracle functionalities
- Implement `sigen_daemon.py` to poll Sigenergy plant metrics and store snapshots. - Create `web_daemon.py` for serving a web interface with various endpoints. - Add debug scripts: - `debug_duplicates.py` to find duplicate target times in forecast data. - `debug_energy_forecast.py` to print baseline energy forecast curves. - `debug_oracle_evaluations.py` to run the oracle evaluator. - `debug_sigen.py` to inspect stored Sigenergy plant snapshots. - `debug_weather.py` to trace resolved truth data. - `modbus_test.py` for exploring Sigenergy plants or inverters over Modbus TCP. - Introduce `oracle_evaluator.py` for evaluating stored oracle predictions against actuals. - Add TCN training scripts in `tcn` directory for training usage sequence models.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from inspect import signature
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
try:
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from pymodbus.exceptions import ModbusException
|
||||
except ImportError: # pragma: no cover - exercised only before dependency install
|
||||
ModbusTcpClient = None # type: ignore[assignment]
|
||||
|
||||
class ModbusException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
RegisterKind = Literal["holding", "input", "coil", "discrete"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModbusReadResult:
|
||||
kind: RegisterKind
|
||||
address: int
|
||||
count: int
|
||||
values: list[int] | list[bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModbusReadError:
|
||||
kind: RegisterKind
|
||||
address: int
|
||||
count: int
|
||||
error: str
|
||||
|
||||
|
||||
class SigenModbusClient:
|
||||
"""Small Modbus TCP client for exploring a Sigenergy plant or inverter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
unit_id: int = 1,
|
||||
timeout: float = 5.0,
|
||||
retries: int = 3,
|
||||
trace: bool = False,
|
||||
) -> None:
|
||||
if ModbusTcpClient is None:
|
||||
raise RuntimeError(
|
||||
"pymodbus is not installed. Install dependencies with "
|
||||
"`python3 -m pip install -r requirements.txt`."
|
||||
)
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.unit_id = unit_id
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.trace = trace
|
||||
self._client = ModbusTcpClient(
|
||||
host=host,
|
||||
port=port,
|
||||
timeout=timeout,
|
||||
retries=retries,
|
||||
trace_packet=self._trace_packet if trace else None,
|
||||
)
|
||||
self._unit_keyword = self._detect_unit_keyword()
|
||||
|
||||
def __enter__(self) -> SigenModbusClient:
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
|
||||
def connect(self) -> None:
|
||||
if not self._client.connect():
|
||||
raise ConnectionError(
|
||||
f"Could not connect to Modbus TCP target {self.host}:{self.port}"
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self._client.close()
|
||||
|
||||
def read(
|
||||
self,
|
||||
kind: RegisterKind,
|
||||
address: int,
|
||||
count: int = 1,
|
||||
) -> ModbusReadResult:
|
||||
if count < 1:
|
||||
raise ValueError("count must be at least 1")
|
||||
if address < 0:
|
||||
raise ValueError("address must be zero or greater")
|
||||
|
||||
response = self._read_raw(kind, address, count)
|
||||
if response.isError():
|
||||
raise ModbusException(str(response))
|
||||
|
||||
values = getattr(response, "registers", None)
|
||||
if values is None:
|
||||
values = getattr(response, "bits", [])
|
||||
values = list(values[:count])
|
||||
|
||||
return ModbusReadResult(
|
||||
kind=kind,
|
||||
address=address,
|
||||
count=count,
|
||||
values=list(values),
|
||||
)
|
||||
|
||||
def scan(
|
||||
self,
|
||||
kind: RegisterKind,
|
||||
start: int,
|
||||
count: int,
|
||||
chunk_size: int = 10,
|
||||
) -> list[ModbusReadResult | ModbusReadError]:
|
||||
if count < 1:
|
||||
raise ValueError("count must be at least 1")
|
||||
if chunk_size < 1:
|
||||
raise ValueError("chunk_size must be at least 1")
|
||||
|
||||
results: list[ModbusReadResult | ModbusReadError] = []
|
||||
stop = start + count
|
||||
address = start
|
||||
while address < stop:
|
||||
current_count = min(chunk_size, stop - address)
|
||||
try:
|
||||
results.append(self.read(kind, address, current_count))
|
||||
except Exception as exc:
|
||||
results.append(
|
||||
ModbusReadError(
|
||||
kind=kind,
|
||||
address=address,
|
||||
count=current_count,
|
||||
error=str(exc),
|
||||
)
|
||||
)
|
||||
address += current_count
|
||||
|
||||
return results
|
||||
|
||||
def _read_raw(self, kind: RegisterKind, address: int, count: int):
|
||||
if kind == "holding":
|
||||
return self._call_read(self._client.read_holding_registers, address, count)
|
||||
if kind == "input":
|
||||
return self._call_read(self._client.read_input_registers, address, count)
|
||||
if kind == "coil":
|
||||
return self._call_read(self._client.read_coils, address, count)
|
||||
if kind == "discrete":
|
||||
return self._call_read(self._client.read_discrete_inputs, address, count)
|
||||
|
||||
raise ValueError(f"Unsupported register kind: {kind}")
|
||||
|
||||
def _call_read(self, method, address: int, count: int):
|
||||
kwargs = {
|
||||
"address": address,
|
||||
"count": count,
|
||||
self._unit_keyword: self.unit_id,
|
||||
}
|
||||
try:
|
||||
return method(**kwargs)
|
||||
except TypeError as exc:
|
||||
if self._unit_keyword not in str(exc):
|
||||
raise
|
||||
|
||||
kwargs.pop(self._unit_keyword)
|
||||
return method(address, self.unit_id, **kwargs)
|
||||
|
||||
def _detect_unit_keyword(self) -> str:
|
||||
read_signature = signature(self._client.read_holding_registers)
|
||||
for keyword in ("device_id", "slave", "unit"):
|
||||
if keyword in read_signature.parameters:
|
||||
return keyword
|
||||
return "slave"
|
||||
|
||||
def _trace_packet(self, sending: bool, packet: bytes) -> bytes:
|
||||
direction = "TX" if sending else "RX"
|
||||
print(f"{direction} {packet.hex(' ')}", file=sys.stderr)
|
||||
return packet
|
||||
Reference in New Issue
Block a user