Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/revert and add more logging for issues #412

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions src/sunsynk/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,51 @@
Returns:
Unpacked integer value
"""
if len(regs) == 1:
fmt = "h" if signed else "H"
return struct.unpack(fmt, struct.pack("H", regs[0]))[0]
if len(regs) == 2:
fmt = "i" if signed else "I"
return struct.unpack(fmt, struct.pack("2H", regs[0], regs[1]))[0]
raise ValueError(f"Unsupported number of registers: {len(regs)}")
try:
# Log suspicious register values (all bits set)
for reg in regs:
if reg == 0xFFFF:
_LOGGER.warning(
"Suspicious register value detected: 0xFFFF (all bits set)"
)

if len(regs) == 1:
fmt = "h" if signed else "H"
try:
return struct.unpack(fmt, struct.pack("H", regs[0]))[0]
except struct.error as e:
_LOGGER.warning(

Check warning on line 60 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L59-L60

Added lines #L59 - L60 were not covered by tests
"Error unpacking single register %s: %s", hex(regs[0]), str(e)
)
raise

Check warning on line 63 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L63

Added line #L63 was not covered by tests

if len(regs) == 2:
fmt = "i" if signed else "I"
try:
val = struct.unpack(fmt, struct.pack("2H", regs[0], regs[1]))[0]
# Log potentially anomalous large values
if abs(val) > 0x7FFFFFFF:
_LOGGER.warning(
"Very large value detected: %s (registers: %s)",
val,
[hex(r) for r in regs],
)
return val
except struct.error as e:
_LOGGER.warning(

Check warning on line 78 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L77-L78

Added lines #L77 - L78 were not covered by tests
"Error unpacking double register %s,%s: %s",
hex(regs[0]),
hex(regs[1]),
str(e),
)
raise

Check warning on line 84 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L84

Added line #L84 was not covered by tests

raise ValueError(f"Unsupported number of registers: {len(regs)}")
except Exception as e:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this outer general exception handler?

you already log 1&2 register exceptions above

_LOGGER.error(

Check warning on line 88 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L86-L88

Added lines #L86 - L88 were not covered by tests
"Failed to unpack registers %s: %s", [hex(r) for r in regs], str(e)
)
raise

Check warning on line 91 in src/sunsynk/helpers.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/helpers.py#L91

Added line #L91 was not covered by tests


def ensure_tuple(val: Any) -> tuple[int, ...]:
Expand Down
92 changes: 85 additions & 7 deletions src/sunsynk/sunsynk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@
import asyncio
import logging
import time
from typing import Iterable, Sequence, TypedDict
from typing import Iterable, Sequence, TypedDict, Union

import attrs

from sunsynk.helpers import hex_str, patch_bitmask
from sunsynk.rwsensors import RWSensor
from sunsynk.sensors import Sensor, ValType
from sunsynk.sensors import (
BinarySensor,
EnumSensor,
InverterStateSensor,
SDStatusSensor,
Sensor,
ValType,
)
from sunsynk.state import InverterState, group_sensors, register_map

_LOGGER = logging.getLogger(__name__)


class RegisterReadError(Exception):
"""Exception raised when there are errors reading registers."""


@attrs.define
class Sunsynk:
"""Sunsync Modbus class."""
Expand Down Expand Up @@ -75,12 +86,19 @@

async def read_sensors(self, sensors: Iterable[Sensor]) -> None:
"""Read a list of sensors - Sunsynk supports function code 0x03."""
# pylint: disable=too-many-locals,too-many-branches
# Check if state is ok & tracking the sensors being read
assert self.state is not None
for sen in sensors:
if sen not in self.state.values:
_LOGGER.warning("sensor %s not being tracked", sen.id)

# Create a map of register addresses to their corresponding sensors
reg_to_sensor: dict[int, Union[Sensor, None]] = {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reg_to_sensor: dict[int, Union[Sensor, None]] = {}
reg_to_sensor: dict[int, Sensor | None] = {}

for sensor in sensors:
for addr in sensor.address:
reg_to_sensor[addr] = sensor

new_regs: dict[int, int] = {}
errs: list[str] = []
groups = group_sensors(
Expand All @@ -92,19 +110,39 @@
glen = grp[-1] - grp[0] + 1
try:
perf = time.perf_counter()
_LOGGER.debug("Starting read of %s registers from %s", glen, grp[0])

r_r = await asyncio.wait_for(
self.read_holding_registers(grp[0], glen), timeout=self.timeout + 1
)
perf = time.perf_counter() - perf

# Log performance metrics
_LOGGER.debug(
"Time taken to fetch %s registers starting at %s : %ss",
glen,
grp[0],
f"{perf:.2f}",
)

# Check for potential communication issues based on timing
if perf > self.timeout * 0.8: # If taking more than 80% of timeout
_LOGGER.warning(

Check warning on line 130 in src/sunsynk/sunsynk.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/sunsynk.py#L130

Added line #L130 was not covered by tests
"Slow register read detected: %ss for %s registers from %s",
f"{perf:.2f}",
glen,
grp[0],
)

except asyncio.TimeoutError:
errs.append(f"timeout reading {glen} registers from {grp[0]}")
self.timeouts += 1
# Log consecutive timeouts
if self.timeouts > 3:
_LOGGER.error(

Check warning on line 142 in src/sunsynk/sunsynk.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/sunsynk.py#L141-L142

Added lines #L141 - L142 were not covered by tests
"Multiple consecutive timeouts detected: %s. Consider checking connection.",
self.timeouts,
)
continue
except Exception as err: # pylint: disable=broad-except
errs.append(
Expand All @@ -117,21 +155,61 @@

if len(r_r) != glen:
_LOGGER.warning(
"Did not complete read, only read %s/%s", len(r_r), glen
"Did not complete read, only read %s/%s registers. This may cause value spikes.",
len(r_r),
glen,
)

# Log register values for debugging
_LOGGER.debug(
"Request registers: %s glen=%d. Response %s len=%d. regs=%s",
grp,
glen,
r_r,
[hex(r) for r in r_r], # Convert to hex for better debugging
len(r_r),
regs,
{
k: hex(v) for k, v in regs.items()
}, # Convert to hex for better debugging
)

self.state.update(new_regs)
# Check for potentially invalid register values
for reg_addr, reg_val in regs.items():
maybe_sensor: Union[Sensor, None] = reg_to_sensor.get(reg_addr)
if maybe_sensor is None:
continue

Check warning on line 179 in src/sunsynk/sunsynk.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/sunsynk.py#L179

Added line #L179 was not covered by tests

current_sensor: Sensor = maybe_sensor
if reg_val == 0xFFFF:
_LOGGER.warning(

Check warning on line 183 in src/sunsynk/sunsynk.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/sunsynk.py#L183

Added line #L183 was not covered by tests
"Potentially invalid register value detected: addr=%s value=0xFFFF sensor=%s",
reg_addr,
current_sensor.name,
)
# Only check zeros for status/enum type sensors
elif reg_val == 0 and any(
isinstance(current_sensor, t)
for t in (
BinarySensor,
EnumSensor,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can’t 0 be a vlid state for Binary/Enum?

InverterStateSensor,
SDStatusSensor,
)
):
_LOGGER.debug(

Check warning on line 198 in src/sunsynk/sunsynk.py

View check run for this annotation

Codecov / codecov/patch

src/sunsynk/sunsynk.py#L198

Added line #L198 was not covered by tests
"Zero value detected in status sensor: addr=%s sensor=%s",
reg_addr,
current_sensor.name,
)

if errs:
raise IOError("; ".join(errs))
_LOGGER.warning("Errors during sensor read: %s", ", ".join(errs))
raise RegisterReadError(", ".join(errs))

# Reset timeout counter if successful
if not errs:
self.timeouts = 0

self.state.update(new_regs)


class SunsynkInitParameters(TypedDict):
Expand Down