Repository: pymodbus-dev/pymodbus

Issue:
minval and maxval for float32 in simulator
<!--
Before opening a new issue, make sure you do the following:

- Check that your issue isn't already filed: https://github.com/pymodbus-dev/pymodbus/issues
- Check the discussions forum https://github.com/pymodbus-dev/pymodbus/discussions
- Prepare a short, runnable example that reproduce the issue with the latest development version of Pymodbus
-->

### Versions

- Python: Python 3.9.2
- OS: Debian 11
- Pymodbus: dev

### Pymodbus Specific

- Server: tcp - Simulator

### Description

<!-- What were you trying, what has happened, what went wrong, and what did you expect? -->
I have this defined in my json file for the simulator.

        {
          "addr": [7, 8],
          "value": 72.0,
          "action": "random",
          "kwargs": { "minval": 65.0, "maxval": 78.0 }
        }

It does not seem to follow the minval and maxval values from what I can see. I am getting random Float values, so that works. But it isn't in between the bounds. Any ideas?




Discussion:
datastore/simulator.py#633 it seems to do what it is supposed to do.

I need to check it.
I see it's not covered in our test suite, I will add a specific test and see what happens.

Resolving patch:
diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py
--- a/pymodbus/datastore/simulator.py
+++ b/pymodbus/datastore/simulator.py
@@ -662,6 +662,8 @@ def action_increment(cls, registers, inx, cell, minval=None, maxval=None):
             value = reg.value + 1
             if maxval and value > maxval:
                 value = minval
+            if minval and value < minval:
+                value = minval
             reg.value = value
         elif cell.type == CellType.FLOAT32:
             tmp_reg = [reg.value, reg2.value]
@@ -669,6 +671,8 @@ def action_increment(cls, registers, inx, cell, minval=None, maxval=None):
             value += 1.0
             if maxval and value > maxval:
                 value = minval
+            if minval and value < minval:
+                value = minval
             new_regs = cls.build_registers_from_value(value, False)
             reg.value = new_regs[0]
             reg2.value = new_regs[1]
@@ -678,6 +682,8 @@ def action_increment(cls, registers, inx, cell, minval=None, maxval=None):
             value += 1
             if maxval and value > maxval:
                 value = minval
+            if minval and value < minval:
+                value = minval
             new_regs = cls.build_registers_from_value(value, True)
             reg.value = new_regs[0]
             reg2.value = new_regs[1]


Test patch:
diff --git a/test/test_simulator.py b/test/test_simulator.py
--- a/test/test_simulator.py
+++ b/test/test_simulator.py
@@ -68,7 +68,7 @@ class TestSimulator:
                 "addr": [19, 20],
                 "value": 14661,
                 "action": "increment",
-                "args": {"min": 1, "max": 100},
+                "args": {"minval": 1, "maxval": 100},
             },
         ],
         "uint32": [
@@ -79,7 +79,7 @@ class TestSimulator:
                 "addr": [31, 32],
                 "value": 50,
                 "action": "random",
-                "kwargs": {"min": 10, "max": 80},
+                "kwargs": {"minval": 10, "maxval": 80},
             },
         ],
         "float32": [
@@ -126,7 +126,9 @@ class TestSimulator:
         Cell(type=CellType.NEXT, value=17320),
         Cell(type=CellType.UINT32, value=5, action=1),
         Cell(type=CellType.NEXT, value=17320),  # 30
-        Cell(type=CellType.UINT32, action=2, action_kwargs={"min": 10, "max": 80}),
+        Cell(
+            type=CellType.UINT32, action=2, action_kwargs={"minval": 10, "maxval": 80}
+        ),
         Cell(type=CellType.NEXT, value=50),
         Cell(type=CellType.FLOAT32, access=True, value=72),
         Cell(type=CellType.NEXT, access=True, value=17221),
@@ -386,7 +388,7 @@ def test_simulator_get_text(self):
                 "31-32",
                 Cell(
                     type=Label.type_uint32,
-                    action="random({'min': 10, 'max': 80})",
+                    action="random({'minval': 10, 'maxval': 80})",
                     value="50",
                 ),
             ),
@@ -435,7 +437,7 @@ def test_simulator_actions(self, func, addr, action):
         assert values[0] or values[1]
 
     def test_simulator_action_timestamp(self):
-        """Test action random"""
+        """Test action timestamp"""
         exc_setup = copy.deepcopy(self.default_config)
         exc_simulator = ModbusSimulatorContext(exc_setup, None)
         addr = 12
@@ -445,7 +447,7 @@ def test_simulator_action_timestamp(self):
         exc_simulator.getValues(FX_READ_REG, addr, 1)
 
     def test_simulator_action_reset(self):
-        """Test action random"""
+        """Test action reset"""
         exc_setup = copy.deepcopy(self.default_config)
         exc_simulator = ModbusSimulatorContext(exc_setup, None)
         addr = 12
@@ -454,3 +456,86 @@ def test_simulator_action_reset(self):
         ]
         with pytest.raises(RuntimeError):
             exc_simulator.getValues(FX_READ_REG, addr, 1)
+
+    @pytest.mark.parametrize(
+        ("celltype", "minval", "maxval", "value", "expected"),
+        [
+            (CellType.BITS, 50, 75, 73, (74, 75, 50)),
+            (CellType.BITS, 50, 75, 45, (50, 51, 52)),
+            (CellType.UINT16, 50, 15075, 15073, (15074, 15075, 50)),
+            (CellType.UINT16, 50, 75, 45, (50, 51, 52)),
+            (CellType.UINT32, 50, 63075, 63073, (63074, 63075, 50)),
+            (CellType.UINT32, 50, 75, 45, (50, 51, 52)),
+            (CellType.FLOAT32, 27.0, 16100.5, 16098.0, (16099.0, 16100.0, 27.0)),
+            (CellType.FLOAT32, 27.0, 75.5, 24.0, (27.0, 28.0, 29.0)),
+        ],
+    )
+    def test_simulator_action_increment(
+        self, celltype, minval, maxval, value, expected
+    ):
+        """Test action increment"""
+        exc_setup = copy.deepcopy(self.default_config)
+        exc_simulator = ModbusSimulatorContext(exc_setup, None)
+        action = exc_simulator.action_name_to_id[Label.increment]
+        kwargs = {
+            "minval": minval,
+            "maxval": maxval,
+        }
+        exc_simulator.registers[30].type = celltype
+        exc_simulator.registers[30].action = action
+        exc_simulator.registers[30].action_kwargs = kwargs
+        exc_simulator.registers[31].type = CellType.NEXT
+
+        is_int = celltype != CellType.FLOAT32
+        reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2
+        regs = (
+            [value, 0]
+            if reg_count == 1
+            else ModbusSimulatorContext.build_registers_from_value(value, is_int)
+        )
+        exc_simulator.registers[30].value = regs[0]
+        exc_simulator.registers[31].value = regs[1]
+        for expect_value in expected:
+            regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count)
+            if reg_count == 1:
+                assert expect_value == regs[0], f"type({celltype})"
+            else:
+                new_value = ModbusSimulatorContext.build_value_from_registers(
+                    regs, is_int
+                )
+                assert expect_value == new_value, f"type({celltype})"
+
+    @pytest.mark.parametrize(
+        ("celltype", "minval", "maxval"),
+        [
+            (CellType.BITS, 50, 75),
+            (CellType.UINT16, 50, 15075),
+            (CellType.UINT32, 50, 63075),
+            (CellType.FLOAT32, 27.0, 16100.5),
+            (CellType.FLOAT32, 65.0, 78.0),
+        ],
+    )
+    def test_simulator_action_random(self, celltype, minval, maxval):
+        """Test action random"""
+        exc_setup = copy.deepcopy(self.default_config)
+        exc_simulator = ModbusSimulatorContext(exc_setup, None)
+        action = exc_simulator.action_name_to_id[Label.random]
+        kwargs = {
+            "minval": minval,
+            "maxval": maxval,
+        }
+        exc_simulator.registers[30].type = celltype
+        exc_simulator.registers[30].action = action
+        exc_simulator.registers[30].action_kwargs = kwargs
+        exc_simulator.registers[31].type = CellType.NEXT
+        is_int = celltype != CellType.FLOAT32
+        reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2
+        for _i in range(100):
+            regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count)
+            if reg_count == 1:
+                new_value = regs[0]
+            else:
+                new_value = ModbusSimulatorContext.build_value_from_registers(
+                    regs, is_int
+                )
+            assert minval <= new_value <= maxval