kellerza / sunsynk

Deye/Sunsynk Inverter Python library and Home Assistant OS Addon
https://kellerza.github.io/sunsynk/
MIT License
204 stars 87 forks source link

Signed value handling in RW sensors #145

Closed jacekowski closed 1 year ago

jacekowski commented 1 year ago

Issue related to

Sunsynk Home Assistant Add-On

Describe the issue/bug

I want to add register 206 (grid trickle feed) sensor definition, while official sunsynk app and web interface does not permit a negative value, writing negative value with a modbus client actually works and provides expected behaviour. I would like to have easy way to automate adjusting this value (to correct for discrepancy between sunsynk meter and revenue meter), so rw sensor seems like a best idea.

However, when i have added my custom sensor definition

SENSORS += NumberRWSensor(206, "Grid Trickle Feed", WATT, 1,min=-500, max=500)

When attempting to write negative value i get a crash.

Expected behavior

Negative value to be written to the inverter

Your environment

Logs

2023-06-02 17:40:39,582 CRITICAL sunsynk.sunsynk Writing sensor grid_trickle_feed=-1 [(206,)=(-1,)]
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/umodbus/functions.py", line 1515, in values
    struct.pack(">" + conf.MULTI_BIT_VALUE_FORMAT_CHARACTER, value)
struct.error: argument out of range

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/app/./run.py", line 320, in <module>
    LOOP.run_until_complete(main(LOOP))
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/usr/src/app/./run.py", line 298, in main
    await write_sensors()
  File "/usr/src/app/./run.py", line 274, in write_sensors
    await STATE[ssi].inv.write_sensor(
  File "/usr/local/lib/python3.9/site-packages/sunsynk/sunsynk.py", line 65, in write_sensor
    await self.write_register(address=addr, value=regs[idx])
  File "/usr/local/lib/python3.9/site-packages/sunsynk/usunsynk.py", line 45, in write_register
    await asyncio.wait_for(
  File "/usr/local/lib/python3.9/asyncio/tasks.py", line 478, in wait_for
    return fut.result()
  File "/usr/local/lib/python3.9/site-packages/async_modbus/core.py", line 233, in write_registers
    request = self.protocol.write_multiple_registers(
  File "/usr/local/lib/python3.9/site-packages/umodbus/client/serial/rtu.py", line 169, in write_multiple_registers
    function.values = values
  File "/usr/local/lib/python3.9/site-packages/umodbus/functions.py", line 1517, in values
    raise IllegalDataValueError
umodbus.exceptions.IllegalDataValueError:  The value contained in the request data field is not an allowable value
    for the server.
kellerza commented 1 year ago

What happens with the pymodbus driver?

jacekowski commented 1 year ago

pymodbus driver doesn't work for me at all (for a different reason)

2023-06-02 18:23:25,216 INFO    options Loading HASS OS configuration
2023-06-02 18:23:25,233 DEBUG   : pymodbus driver options: {'port': 'serial-tcp://10.42.1.176:502', 'server_id': 1, 'timeout': 10, 'read_sensors_batch_size': 60}
2023-06-02 18:23:25,233 INFO    sensors Using Single phase sensor definitions.
2023-06-02 18:23:25,234 INFO    helpers Importing /share/hass-addon-sunsynk/mysensors.py...
2023-06-02 18:23:25,234 INFO    helpers   custom sensors: grid_trickle_feed, grid_trickle_feed_ro
2023-06-02 18:23:25,234 DEBUG   filter step unit:W, id:rated_power
2023-06-02 18:23:25,235 DEBUG   asyncio Using selector: EpollSelector
2023-06-02 18:23:25,235 INFO    : Connecting to serial-tcp://10.42.1.176:502
2023-06-02 18:23:25,235 INFO    sunsynk.pysunsynk PyModbus 3.2.2 TCP: 10.42.1.176:502
2023-06-02 18:23:25,235 DEBUG   pymodbus.logging Connecting to 10.42.1.176:502.
2023-06-02 18:23:25,235 DEBUG   pymodbus.logging Connecting.
2023-06-02 18:23:25,245 DEBUG   pymodbus.logging Client connected to modbus server
2023-06-02 18:23:25,247 INFO    pymodbus.logging Protocol made connection.
2023-06-02 18:23:25,247 DEBUG   asyncio <asyncio.TransportSocket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('172.30.33.2', 55844), raddr=('10.42.1.176', 502)> connected to 10.42.1.176:502: (<_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>, <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fb67e6fa700>)
2023-06-02 18:23:25,248 INFO    pymodbus.logging Connected to 10.42.1.176:502.
2023-06-02 18:23:25,248 INFO    : Reading startup sensors rated_power, serial, battery_shutdown_capacity, battery_low_capacity, prog6_time, prog2_time, prog1_time, prog3_time, prog4_time, prog5_time
2023-06-02 18:23:25,249 DEBUG   pymodbus.logging send: 0x0 0x1 0x0 0x0 0x0 0x6 0x1 0x3 0x0 0x3 0x0 0x5
2023-06-02 18:23:25,249 DEBUG   pymodbus.logging Adding transaction 1
2023-06-02 18:23:28,284 INFO    pymodbus.logging Protocol lost connection.
2023-06-02 18:23:28,287 DEBUG   pymodbus.logging Client disconnected from modbus server: trying to send
2023-06-02 18:23:28,287 DEBUG   pymodbus.logging Getting transaction 1
2023-06-02 18:23:28,288 INFO    pymodbus.logging Protocol lost connection.
2023-06-02 18:23:28,288 WARNING pymodbus.logging Ignoring launch of delayed reconnection, another is in progress
2023-06-02 18:23:28,289 DEBUG   pymodbus.logging Client disconnected from modbus server: None
2023-06-02 18:23:28,289 DEBUG   pymodbus.logging Waiting 100 ms before next connection attempt.
2023-06-02 18:23:28,290 ERROR   sunsynk.sunsynk timeout reading register 3 (5)
2023-06-02 18:23:28,290 INFO    : ############################################################
2023-06-02 18:23:28,290 INFO    : No response on the Modbus interface serial-tcp://10.42.1.176:502, try checking the wiring to the Inverter, the USB-to-RS485 converter, etc
2023-06-02 18:23:28,290 INFO    : ############################################################
2023-06-02 18:23:28,290 CRITICAL : This Add-On will terminate in 30 seconds, use the Supervisor Watchdog to restart automatically.
2023-06-02 18:23:28,393 DEBUG   pymodbus.logging Connecting.
2023-06-02 18:23:28,403 DEBUG   pymodbus.logging Client connected to modbus server
2023-06-02 18:23:28,403 INFO    pymodbus.logging Protocol made connection.
2023-06-02 18:23:28,403 DEBUG   asyncio <asyncio.TransportSocket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('172.30.33.2', 55856), raddr=('10.42.1.176', 502)> connected to 10.42.1.176:502: (<_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>, <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fb67e6fa700>)
2023-06-02 18:23:28,404 INFO    pymodbus.logging Connected to 10.42.1.176:502.
kellerza commented 1 year ago

If you just have PORT: tcp://10.42.1.176:502 instead of serial-tcp://?

kellerza commented 1 year ago

btw, what other modbus client did you write it with?

jacekowski commented 1 year ago

just tcp:// doesn't work (my converter does not convert modbus tcp to modbus rtu so i need the addon to send modbus rtu over tcp)

The client i'm using is Modbus Poll

kellerza commented 1 year ago

Ok, my only suggestion for now is that you do the inverse of this - https://github.com/kellerza/sunsynk/blob/main/sunsynk/helpers.py#L49

So for a negative number, add 0xFFFF before you write it

jacekowski commented 1 year ago

That's what i'm doing now (in node red), and i have found another bug, when using Sensor with -1 factor the resulting value is off by 1.

image

SENSORS += NumberRWSensor(206, "Grid Trickle Feed", WATT, 1,max=65536)
SENSORS += Sensor(206, "Grid Trickle Feed RO", WATT, -1)

however, 65466 = 0xFFBA, Signed 0xFFBA = -70

kellerza commented 1 year ago

Try this in mysensors. If it works we need to replace NumberRWSensors class & fix the function I linked above for signed

import attrs
from typing import Union
from sunsynk import WATT
from sunsynk.rwsensors import ResolveType, NumberRWSensor, resolve_num
from sunsynk.sensors import RegType, Sensor, SensorDefinitions, ValType

SENSORS = SensorDefinitions()

@attrs.define(slots=True, eq=False)
class NumberRWSensor2(NumberRWSensor):
    """Numeric sensor which can be read and written."""

    min: Union[int, float, Sensor] = attrs.field(default=0)
    max: Union[int, float, Sensor] = attrs.field(default=100)

    @property
    def dependencies(self) -> list[Sensor]:
        """Get a list of sensors upon which this sensor depends."""
        return [s for s in (self.min, self.max) if isinstance(s, Sensor)]

    def reg_to_value(self, regs: RegType) -> ValType:
        """Decode the register."""
        res = super().reg_to_value(self.masked(regs))
        return res if res >= 0 else res - 1  # signed needs to -0x10000

    def value_to_reg(self, value: ValType, resolve: ResolveType) -> RegType:
        """Get the reg value from a display value, or the current reg value if out of range."""
        fval = float(value)  # type:ignore
        minv = resolve_num(resolve, self.min, 0)
        maxv = resolve_num(resolve, self.max, 100)
        val = int(max(minv, min(maxv, fval / abs(self.factor))))
        if len(self.address) == 1:
            if val < 0:
                val = 0x10000 + val
            return self.reg(val)
        if len(self.address) == 2:
            return self.reg(val & 0xFFFF, int(val >> 16))
        raise NotImplementedError

SENSORS += NumberRWSensor2(206, "Grid Trickle Feed", WATT, -1, min=-500, max=500)
jacekowski commented 1 year ago

It works.