From fd68012792387f7ed43d847501522d0215e58b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Tue, 28 Oct 2025 13:17:41 +0100 Subject: [PATCH 01/37] feat(hz): Add getter to read Hertz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added tests to test function Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 41 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 4da7c7e..2f83fe9 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -28,6 +28,7 @@ class Em511: EM511_REGISTER_V = 0x0000 EM511_REGISTER_A = 0x0002 + EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 SCALE_10 = 10 @@ -151,6 +152,21 @@ def A(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_A)) / self.SCALE_1000 return round(value, 3) + @property + def Hz(self) -> Decimal: + """Hertz (Hz). + + Returns: + Decimal: Current hertz value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_HZ, self.INT16_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_HZ)) / self.SCALE_10 + return round(value, 1) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 8c8bac5..6a5b5a5 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -2,6 +2,7 @@ """Test file for driver.""" +from decimal import Decimal from unittest.mock import MagicMock import pytest @@ -20,13 +21,13 @@ def test_V() -> None: mock_result.registers = [0x08FC, 0x0000] client.read_input_registers.return_value = mock_result value = meter.V - assert value == 230 + assert value == Decimal("230.0") """Test 2: Should pass.""" mock_result.registers = [0x9A28, 0x0001] client.read_input_registers.return_value = mock_result value = meter.V - assert value == 10500 + assert value == Decimal("10500.0") """Test 3: Should raise exception due to more registers in use than allowed.""" mock_result.registers = [0x1860, 0x0023, 0x4244] @@ -52,13 +53,13 @@ def test_get_A() -> None: mock_result.registers = [0x2904, 0x0000] client.read_input_registers.return_value = mock_result value = meter.A - assert value == 10.5 + assert value == Decimal("10.5") """Test 2: Should pass.""" mock_result.registers = [0x1860, 0x0023] client.read_input_registers.return_value = mock_result value = meter.A - assert value == 2300 + assert value == Decimal("2300.0") """Test 3: Should raise exception due to more registers in use than allowed.""" mock_result.registers = [0x1860, 0x0023, 0x4244] @@ -73,6 +74,38 @@ def test_get_A() -> None: _ = meter.A +def test_get_Hz() -> None: + """Test Get HZ.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x08FC] + client.read_input_registers.return_value = mock_result + value = meter.Hz + assert value == Decimal("230.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x9A28] + client.read_input_registers.return_value = mock_result + value = meter.Hz + assert value == Decimal("3946.4") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.Hz + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.Hz + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From 263e0ee9826dfb9cdc10393c047ea2936efeef24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Tue, 28 Oct 2025 13:26:41 +0100 Subject: [PATCH 02/37] feat(kwh-tot): Add getter for total kWh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests for the functio. Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 20 ++++++++++++++++++-- src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 2f83fe9..d7b6ba3 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -26,10 +26,11 @@ class Em511: INPUT_MAX_VALUE_32 = 0x7FFFFFFF INPUT_MAX_VALUE_16 = 0x7FFF - EM511_REGISTER_V = 0x0000 - EM511_REGISTER_A = 0x0002 + EM511_REGISTER_V = 0x0 + EM511_REGISTER_A = 0x2 EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 + EM511_REGISTER_KWH_TOT = 0x10 SCALE_10 = 10 SCALE_100 = 100 @@ -167,6 +168,21 @@ def Hz(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_HZ)) / self.SCALE_10 return round(value, 1) + @property + def kWh_tot(self) -> Decimal: + """Kilo watt hours in total (kWh). + + Returns: + Decimal: Current total kWh value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_KWH_TOT, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_KWH_TOT)) / self.SCALE_10 + return round(value, 1) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 6a5b5a5..a1a0468 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -106,6 +106,38 @@ def test_get_Hz() -> None: _ = meter.Hz +def test_kWh_tot() -> None: + """Test Get total kWh.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x08FC, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.kWh_tot + assert value == Decimal("230.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x9A28, 0x0001] + client.read_input_registers.return_value = mock_result + value = meter.kWh_tot + assert value == Decimal("10500.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.kWh_tot + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.kWh_tot + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From 158beeb6c7d3a75db31786fa2ce1110e3a4114b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Tue, 28 Oct 2025 13:33:53 +0100 Subject: [PATCH 03/37] feat(kwh-partial): Add getter to read partial kWh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid test for the function Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index d7b6ba3..520697e 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -31,6 +31,7 @@ class Em511: EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 + EM511_REGISTER_KWH_PARTIAL = 0x14 SCALE_10 = 10 SCALE_100 = 100 @@ -183,6 +184,21 @@ def kWh_tot(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_KWH_TOT)) / self.SCALE_10 return round(value, 1) + @property + def kWh_partial(self) -> Decimal: + """Kilo watt hours partial (kWh). + + Returns: + Decimal: Current partial kWh value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_KWH_PARTIAL, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_KWH_PARTIAL)) / self.SCALE_10 + return round(value, 1) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index a1a0468..13d6d93 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -138,6 +138,38 @@ def test_kWh_tot() -> None: _ = meter.kWh_tot +def test_kWh_partial() -> None: + """Test Get partial kWh.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x08FC, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.kWh_partial + assert value == Decimal("230.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x9A28, 0x0001] + client.read_input_registers.return_value = mock_result + value = meter.kWh_partial + assert value == Decimal("10500.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.kWh_partial + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.kWh_partial + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From 076eed1ac36119504c7ecb16063f536536d5d18b Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Tue, 28 Oct 2025 14:35:15 -0400 Subject: [PATCH 04/37] feat(w): Add getter to read current power(w) value Also added valid tests for function Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 520697e..3f3aafe 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -28,6 +28,7 @@ class Em511: EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 + EM511_REGISTER_W = 0x4 EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 @@ -154,6 +155,21 @@ def A(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_A)) / self.SCALE_1000 return round(value, 3) + @property + def W(self) -> Decimal: + """Power (W). + + Returns: + Decimal: Current watt value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_W, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_W)) / self.SCALE_10 + return round(value, 1) + @property def Hz(self) -> Decimal: """Hertz (Hz). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 13d6d93..e9c8082 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -74,6 +74,38 @@ def test_get_A() -> None: _ = meter.A +def test_get_W() -> None: + """Test Get w.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.W + assert value == Decimal("1050.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.W + assert value == Decimal("230000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.W + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.W + + def test_get_Hz() -> None: """Test Get HZ.""" client = MagicMock() From 7028ec99900ead1eb8f9a785037671e54d8ba084 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Tue, 28 Oct 2025 14:41:24 -0400 Subject: [PATCH 05/37] feat(w-dmd): add getter to read current power demand(w) also added valid tests. Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 3f3aafe..acefc1e 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -29,6 +29,7 @@ class Em511: EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 EM511_REGISTER_W = 0x4 + EM511_REGISTER_W_DMD = 0xA EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 @@ -170,6 +171,21 @@ def W(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_W)) / self.SCALE_10 return round(value, 1) + @property + def W_dmd(self) -> Decimal: + """Power (W_dmd). + + Returns: + Decimal: Current watt demand value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_W_DMD, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_W_DMD)) / self.SCALE_10 + return round(value, 1) + @property def Hz(self) -> Decimal: """Hertz (Hz). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index e9c8082..e385ef3 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -106,6 +106,38 @@ def test_get_W() -> None: _ = meter.W +def test_get_W_dmd() -> None: + """Test Get w_dmd.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.W_dmd + assert value == Decimal("1050.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.W_dmd + assert value == Decimal("230000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.W_dmd + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.W_dmd + + def test_get_Hz() -> None: """Test Get HZ.""" client = MagicMock() From 047f28d13087983dd28afc10b53062f80082a1f0 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Tue, 28 Oct 2025 14:45:27 -0400 Subject: [PATCH 06/37] feat(w-dmd-peak): add getter power demand peak(w) also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index acefc1e..b3005a5 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -30,6 +30,7 @@ class Em511: EM511_REGISTER_A = 0x2 EM511_REGISTER_W = 0x4 EM511_REGISTER_W_DMD = 0xA + EM511_REGISTER_W_DMD_PEAK = 0xC EM511_REGISTER_HZ = 0xF EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 @@ -186,6 +187,21 @@ def W_dmd(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_W_DMD)) / self.SCALE_10 return round(value, 1) + @property + def W_dmd_peak(self) -> Decimal: + """Power (W_dmd_peak). + + Returns: + Decimal: Current watt demand peak value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_W_DMD_PEAK, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_W_DMD_PEAK)) / self.SCALE_10 + return round(value, 1) + @property def Hz(self) -> Decimal: """Hertz (Hz). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index e385ef3..ebc6749 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -138,6 +138,38 @@ def test_get_W_dmd() -> None: _ = meter.W_dmd +def test_get_W_dmd_peak() -> None: + """Test Get w_dmd_peak.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.W_dmd_peak + assert value == Decimal("1050.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.W_dmd_peak + assert value == Decimal("230000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.W_dmd_peak + + """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.W_dmd_peak + + def test_get_Hz() -> None: """Test Get HZ.""" client = MagicMock() From 975a5ee22693b6dcb28ca9cac7c64f26c879e67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Tue, 28 Oct 2025 15:44:02 +0100 Subject: [PATCH 07/37] feat(device-id): Add getter to read device id/address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests for the function Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 24 ++++++++++++++++++ src/em511/test_em511.py | 54 +++++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index b3005a5..158f94f 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -25,6 +25,8 @@ class Em511: PASSWORD_MAX_VALUE = 9999 INPUT_MAX_VALUE_32 = 0x7FFFFFFF INPUT_MAX_VALUE_16 = 0x7FFF + DEVICE_ADDRESS_MAX_VALUE = 247 + DEVICE_ADDRESS_MIN_VALUE = 1 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -35,6 +37,7 @@ class Em511: EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 EM511_REGISTER_KWH_PARTIAL = 0x14 + EM511_REGISTER_DEVICE_ID = 0x2000 SCALE_10 = 10 SCALE_100 = 100 @@ -266,6 +269,27 @@ def password(self) -> int: raise ValueError(msg) return value + @property + def device_id(self) -> int: + """Device address. + + (Default value=1) + + Returns: + Int: Current device address. + + Raises: + ValueError: If input is at max value or above. + ValueError: If device address is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_DEVICE_ID, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_DEVICE_ID) + if not (self.DEVICE_ADDRESS_MIN_VALUE <= value <= self.DEVICE_ADDRESS_MAX_VALUE): + msg = f"Invalid device address value: {value}. Must be between 1 and 247." + raise ValueError(msg) + return value + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index ebc6749..48819cc 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -35,7 +35,7 @@ def test_V() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.V - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -67,7 +67,7 @@ def test_get_A() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.A - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -99,7 +99,7 @@ def test_get_W() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.W - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -131,7 +131,7 @@ def test_get_W_dmd() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.W_dmd - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -163,7 +163,7 @@ def test_get_W_dmd_peak() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.W_dmd_peak - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -195,7 +195,7 @@ def test_get_Hz() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.Hz - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" mock_result.registers = [0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): @@ -227,7 +227,7 @@ def test_kWh_tot() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.kWh_tot - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -259,7 +259,7 @@ def test_kWh_partial() -> None: with pytest.raises(ValueError, match="Unexpected register count:"): _ = meter.kWh_partial - """Test 6: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" mock_result.registers = [0xFFFF, 0x7FFF] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): @@ -292,6 +292,44 @@ def test_get_password() -> None: _ = meter.password +def test_get_device_id() -> None: + """Test Get device id/address.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0xF7] + client.read_input_registers.return_value = mock_result + value = meter.device_id + assert value == 247 + + """Test 2: Should pass.""" + mock_result.registers = [0x1] + client.read_input_registers.return_value = mock_result + value = meter.device_id + assert value == 1 + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x1860] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid device address value: "): + _ = meter.device_id + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.device_id + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.device_id + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From 654f98cf86f044e70cf215a0a839aad476c6a909 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Tue, 28 Oct 2025 16:13:33 -0400 Subject: [PATCH 08/37] feat(run-hour-meter): add getter to read run time in hours also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 158f94f..3537ae2 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -37,6 +37,7 @@ class Em511: EM511_REGISTER_PASSWORD = 0x1000 EM511_REGISTER_KWH_TOT = 0x10 EM511_REGISTER_KWH_PARTIAL = 0x14 + EM511_REGISTER_HOUR_COUNTER = 0x2C EM511_REGISTER_DEVICE_ID = 0x2000 SCALE_10 = 10 @@ -250,6 +251,21 @@ def kWh_partial(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_KWH_PARTIAL)) / self.SCALE_10 return round(value, 1) + @property + def hour_counter(self) -> Decimal: + """Run time in hours (h). + + Returns: + Decimal: Run time in hours value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_HOUR_COUNTER, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_HOUR_COUNTER)) / self.SCALE_100 + return round(value, 2) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 48819cc..9e06936 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -266,6 +266,38 @@ def test_kWh_partial() -> None: _ = meter.kWh_partial +def test_get_hour_counter() -> None: + """Test Get hour_counter.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.hour_counter + assert value == Decimal("105.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.hour_counter + assert value == Decimal("23000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.hour_counter + + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.hour_counter + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From dd8539230e6b6f161f4ea070530a17d35cf8bdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 08:31:11 +0100 Subject: [PATCH 09/37] feat(device-id): Add getter to read device id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 25 +++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 3537ae2..1657203 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -27,6 +27,8 @@ class Em511: INPUT_MAX_VALUE_16 = 0x7FFF DEVICE_ADDRESS_MAX_VALUE = 247 DEVICE_ADDRESS_MIN_VALUE = 1 + BAUD_RATE_MIN_VALUE = 1 + BAUD_RATE_MAX_VALUE = 5 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -39,6 +41,7 @@ class Em511: EM511_REGISTER_KWH_PARTIAL = 0x14 EM511_REGISTER_HOUR_COUNTER = 0x2C EM511_REGISTER_DEVICE_ID = 0x2000 + EM511_REGISTER_BAUD_RATE = 0x2001 SCALE_10 = 10 SCALE_100 = 100 @@ -306,6 +309,28 @@ def device_id(self) -> int: raise ValueError(msg) return value + @property + def baud_rate(self) -> int: + """Baud rate. + + 1=9.6kbps, 2=19.2kbps, 3=38.4kbps, + 4=57.6kbps, 5=115.2kbps (default=1) + + Returns: + Int: Current baud rate. + + Raises: + ValueError: If input is at max value or above. + ValueError: If baud rate is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_BAUD_RATE, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_BAUD_RATE) + if not (self.BAUD_RATE_MIN_VALUE <= value <= self.BAUD_RATE_MAX_VALUE): + msg = f"Invalid baud rate value: {value}. Must be between 1 and 5." + raise ValueError(msg) + return value + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 9e06936..bd58ec0 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -362,6 +362,44 @@ def test_get_device_id() -> None: _ = meter.device_id +def test_get_baud_rate() -> None: + """Test Get baud_rate.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x5] + client.read_input_registers.return_value = mock_result + value = meter.baud_rate + assert value == 5 + + """Test 2: Should pass.""" + mock_result.registers = [0x1] + client.read_input_registers.return_value = mock_result + value = meter.baud_rate + assert value == 1 + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x6] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid baud rate value:"): + _ = meter.baud_rate + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.baud_rate + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.baud_rate + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From 7824f560f4e7dd3a0ea2e00a61ebe436f8fccee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 08:41:01 +0100 Subject: [PATCH 10/37] feat(baud-rate): Add getter to read baud rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 24 ++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 1657203..2928cee 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -29,6 +29,8 @@ class Em511: DEVICE_ADDRESS_MIN_VALUE = 1 BAUD_RATE_MIN_VALUE = 1 BAUD_RATE_MAX_VALUE = 5 + PARITY_MIN_VALUE = 1 + PARITY_MAX_VALUE = 2 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -42,6 +44,7 @@ class Em511: EM511_REGISTER_HOUR_COUNTER = 0x2C EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 + EM511_REGISTER_PARITY = 0x2002 SCALE_10 = 10 SCALE_100 = 100 @@ -331,6 +334,27 @@ def baud_rate(self) -> int: raise ValueError(msg) return value + @property + def parity(self) -> int: + """Parity. + + 1=None, 2=Even (default=1) + + Returns: + Int: Current parity. + + Raises: + ValueError: If input is at max value or above. + ValueError: If parity is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_PARITY, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_PARITY) + if not (self.PARITY_MIN_VALUE <= value <= self.PARITY_MAX_VALUE): + msg = f"Invalid parity value: {value}. Must be between 1 and 2." + raise ValueError(msg) + return value + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index bd58ec0..1fdd99b 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -400,6 +400,44 @@ def test_get_baud_rate() -> None: _ = meter.baud_rate +def test_get_parity() -> None: + """Test Get parity.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2] + client.read_input_registers.return_value = mock_result + value = meter.parity + assert value == 2 + + """Test 2: Should pass.""" + mock_result.registers = [0x1] + client.read_input_registers.return_value = mock_result + value = meter.parity + assert value == 1 + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x3] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid parity value:"): + _ = meter.parity + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.parity + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.parity + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From 82a6dd7bd4a46e75b975a89cf2cbb48ecb0460b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 08:50:55 +0100 Subject: [PATCH 11/37] feat(stop-bit): Add getter to read stop bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 24 ++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 2928cee..3677b16 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -31,6 +31,8 @@ class Em511: BAUD_RATE_MAX_VALUE = 5 PARITY_MIN_VALUE = 1 PARITY_MAX_VALUE = 2 + STOP_BIT_MIN_VALUE = 1 + STOP_BIT_MAX_VALUE = 2 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -45,6 +47,7 @@ class Em511: EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 + EM511_REGISTER_STOP_BIT = 0x2003 SCALE_10 = 10 SCALE_100 = 100 @@ -355,6 +358,27 @@ def parity(self) -> int: raise ValueError(msg) return value + @property + def stop_bit(self) -> int: + """Stop bits. + + 0=1 stop bit (default), 1=2 stop bits; fixed to 1 if parity=Even + + Returns: + Int: Current stop bit value. + + Raises: + ValueError: If input is at max value or above. + ValueError: If stop bit is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_STOP_BIT, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_STOP_BIT) + if not (self.STOP_BIT_MIN_VALUE <= value <= self.STOP_BIT_MAX_VALUE): + msg = f"Invalid stop bit value: {value}. Must be between 1 and 2." + raise ValueError(msg) + return value + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 1fdd99b..0f8183e 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -438,6 +438,44 @@ def test_get_parity() -> None: _ = meter.parity +def test_get_stop_bit() -> None: + """Test Get stop_bit.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2] + client.read_input_registers.return_value = mock_result + value = meter.stop_bit + assert value == 2 + + """Test 2: Should pass.""" + mock_result.registers = [0x1] + client.read_input_registers.return_value = mock_result + value = meter.stop_bit + assert value == 1 + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x3] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid stop bit value:"): + _ = meter.stop_bit + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.stop_bit + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.stop_bit + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From cbb5fe499d78b0e56efd3c9f8807d51a90adab66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 09:05:20 +0100 Subject: [PATCH 12/37] feat(reply-delay): Add getter to read reply delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 24 ++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 3677b16..84a39ef 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -33,6 +33,8 @@ class Em511: PARITY_MAX_VALUE = 2 STOP_BIT_MIN_VALUE = 1 STOP_BIT_MAX_VALUE = 2 + REPLY_DELAY_MIN_VALUE = 0 + REPLY_DELAY_MAX_VALUE = 1000 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -48,6 +50,7 @@ class Em511: EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 EM511_REGISTER_STOP_BIT = 0x2003 + EM511_REGISTER_REPLY_DELAY = 0x2004 SCALE_10 = 10 SCALE_100 = 100 @@ -379,6 +382,27 @@ def stop_bit(self) -> int: raise ValueError(msg) return value + @property + def reply_delay(self) -> int: + """Reply delay. + + Range: 0-1000 ms (default=0) + + Returns: + int: Current reply delay value. + + Raises: + ValueError: If input is at max value or above. + ValueError: If reply delay is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_REPLY_DELAY, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_REPLY_DELAY) + if not (self.REPLY_DELAY_MIN_VALUE <= value <= self.REPLY_DELAY_MAX_VALUE): + msg = f"Invalid reply delay value: {value}. Must be between 0 and 1000." + raise ValueError(msg) + return value + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 0f8183e..37c4e31 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -476,6 +476,44 @@ def test_get_stop_bit() -> None: _ = meter.stop_bit +def test_get_reply_delay() -> None: + """Test Get reply delay.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x3E8] + client.read_input_registers.return_value = mock_result + value = meter.reply_delay + assert value == 1000 + + """Test 2: Should pass.""" + mock_result.registers = [0x0] + client.read_input_registers.return_value = mock_result + value = meter.reply_delay + assert value == 0 + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x3E9] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid reply delay value:"): + _ = meter.reply_delay + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x5743] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.reply_delay + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.reply_delay + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From c63dad67daadaf0d14139528b25773c602e18a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 09:11:24 +0100 Subject: [PATCH 13/37] feat(device-id): Add setter to set device id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 19 +++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 84a39ef..1b2742c 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -421,3 +421,22 @@ def password(self, value: int) -> None: msg = f"Invalid password value: {value}. Must be between 0 and 9999." raise ValueError(msg) self._write_register(self.EM511_REGISTER_PASSWORD, value) + + @device_id.setter + def device_id(self, value: int) -> None: + """Device id. + + Value: + Range 1 - 247 + + Args: + value (int): Set device id. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If device id value is out of range. + """ + if not (self.DEVICE_ADDRESS_MIN_VALUE <= value <= self.DEVICE_ADDRESS_MAX_VALUE): + msg = f"Invalid device id value: {value}. Must be between 1 and 247." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_DEVICE_ID, value) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 37c4e31..e5ed3cb 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -546,3 +546,41 @@ def test_set_password() -> None: client.write_register.return_value = mock_result meter.password = 0 client.write_register.assert_called_once_with(address=4096, value=0, device_id=1) + + +def test_set_device_id() -> None: + """Test Set device id.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set device id""" + mock_result.registers = [8192] + client.write_register.return_value = mock_result + meter.device_id = 123 + client.write_register.assert_called_once_with(address=8192, value=123, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set device id out of range.""" + with pytest.raises(ValueError, match="Invalid device id value:"): + meter.device_id = 248 + + """Test 3: Try set device id out of range.""" + with pytest.raises(ValueError, match="Invalid device id value:"): + meter.device_id = 0 + + """Test 4: Try set device id at maximum value.""" + mock_result.registers = [8192] + client.write_register.return_value = mock_result + meter.device_id = 247 + client.write_register.assert_called_once_with(address=8192, value=247, device_id=1) + + client.write_register.reset_mock() + + """Test 5: Try set device id at lowest value.""" + mock_result.registers = [8192] + client.write_register.return_value = mock_result + meter.device_id = 1 + client.write_register.assert_called_once_with(address=8192, value=1, device_id=1) From 546c0bc6556944ec7fcbf603d461c6fd3bda5f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 09:27:07 +0100 Subject: [PATCH 14/37] feat(baud-rate): Add setter to set baud rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 22 ++++++++++++++++++++-- src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 1b2742c..e0aa2a3 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -426,8 +426,7 @@ def password(self, value: int) -> None: def device_id(self, value: int) -> None: """Device id. - Value: - Range 1 - 247 + Range 1 - 247 Args: value (int): Set device id. @@ -440,3 +439,22 @@ def device_id(self, value: int) -> None: msg = f"Invalid device id value: {value}. Must be between 1 and 247." raise ValueError(msg) self._write_register(self.EM511_REGISTER_DEVICE_ID, value) + + @baud_rate.setter + def baud_rate(self, value: int) -> None: + """Baud rate. + + 1=9.6kbps, 2=19.2kbps, 3=38.4kbps, + 4=57.6kbps, 5=115.2kbps (default=1). + + Args: + value (int): Set baud rate. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If baud rate value is out of range. + """ + if not (self.BAUD_RATE_MIN_VALUE <= value <= self.BAUD_RATE_MAX_VALUE): + msg = f"Invalid baud rate value: {value}. Must be between 1 and 5." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_BAUD_RATE, value) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index e5ed3cb..7222399 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -584,3 +584,41 @@ def test_set_device_id() -> None: client.write_register.return_value = mock_result meter.device_id = 1 client.write_register.assert_called_once_with(address=8192, value=1, device_id=1) + + +def test_set_baud_rate() -> None: + """Test Set baud rate.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set baud rate""" + mock_result.registers = [8193] + client.write_register.return_value = mock_result + meter.baud_rate = 2 + client.write_register.assert_called_once_with(address=8193, value=2, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set baud rate out of range.""" + with pytest.raises(ValueError, match="Invalid baud rate value:"): + meter.baud_rate = 6 + + """Test 3: Try set baud rate out of range.""" + with pytest.raises(ValueError, match="Invalid baud rate value:"): + meter.baud_rate = 0 + + """Test 4: Try set baud rate at maximum value.""" + mock_result.registers = [8193] + client.write_register.return_value = mock_result + meter.baud_rate = 5 + client.write_register.assert_called_once_with(address=8193, value=5, device_id=1) + + client.write_register.reset_mock() + + """Test 5: Try set baud rate at lowest value.""" + mock_result.registers = [8193] + client.write_register.return_value = mock_result + meter.baud_rate = 1 + client.write_register.assert_called_once_with(address=8193, value=1, device_id=1) From 5b8d57c576091ea8104335330cf51e6555077a57 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 09:33:02 -0400 Subject: [PATCH 15/37] feat(life-time-counter): add getter to read life time count in hours also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index e0aa2a3..86493c2 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -46,6 +46,7 @@ class Em511: EM511_REGISTER_KWH_TOT = 0x10 EM511_REGISTER_KWH_PARTIAL = 0x14 EM511_REGISTER_HOUR_COUNTER = 0x2C + EM511_REGISTER_LIFETIME_COUNTER = 0x30 EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 @@ -278,6 +279,21 @@ def hour_counter(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_HOUR_COUNTER)) / self.SCALE_100 return round(value, 2) + @property + def lifetime_counter(self) -> Decimal: + """Lifetime in hours (h). + + Returns: + Decimal: Lifetime in hours value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_LIFETIME_COUNTER, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_LIFETIME_COUNTER)) / self.SCALE_100 + return round(value, 2) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 7222399..35042da 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -298,6 +298,38 @@ def test_get_hour_counter() -> None: _ = meter.hour_counter +def test_get_lifetime_counter() -> None: + """Test Get lifetime_counter.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.lifetime_counter + assert value == Decimal("105.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.lifetime_counter + assert value == Decimal("23000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.lifetime_counter + + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.lifetime_counter + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From 5fb1a9cb39610a977d459328ddd00ea97c95d9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 09:43:39 +0100 Subject: [PATCH 16/37] feat(parity): Add setter to set parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 19 +++++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 86493c2..2f63724 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -474,3 +474,22 @@ def baud_rate(self, value: int) -> None: msg = f"Invalid baud rate value: {value}. Must be between 1 and 5." raise ValueError(msg) self._write_register(self.EM511_REGISTER_BAUD_RATE, value) + + @parity.setter + def parity(self, value: int) -> None: + """Parity. + + 1=None, 2=Even (default=1). + + + Args: + value (int): Set parity. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If parity value is out of range. + """ + if not (self.PARITY_MIN_VALUE <= value <= self.PARITY_MAX_VALUE): + msg = f"Invalid parity value: {value}. Must be between 1 and 2." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_PARITY, value) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 35042da..05c1544 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -654,3 +654,35 @@ def test_set_baud_rate() -> None: client.write_register.return_value = mock_result meter.baud_rate = 1 client.write_register.assert_called_once_with(address=8193, value=1, device_id=1) + + +def test_set_parity() -> None: + """Test Set parity.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set parity""" + mock_result.registers = [8194] + client.write_register.return_value = mock_result + meter.parity = 1 + client.write_register.assert_called_once_with(address=8194, value=1, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set parity out of range.""" + with pytest.raises(ValueError, match="Invalid parity value:"): + meter.parity = 3 + + """Test 3: Try set parity out of range.""" + with pytest.raises(ValueError, match="Invalid parity value:"): + meter.parity = 0 + + """Test 4: Try set parity at maximum value.""" + mock_result.registers = [8194] + client.write_register.return_value = mock_result + meter.parity = 2 + client.write_register.assert_called_once_with(address=8194, value=2, device_id=1) + + client.write_register.reset_mock() From 81e47076bda81642fce2b2e9ec46309300a3b89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 09:49:32 +0100 Subject: [PATCH 17/37] feat(stop-bit): Add setter to set stop bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 22 ++++++++++++++++++++-- src/em511/test_em511.py | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 2f63724..667495c 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -31,8 +31,8 @@ class Em511: BAUD_RATE_MAX_VALUE = 5 PARITY_MIN_VALUE = 1 PARITY_MAX_VALUE = 2 - STOP_BIT_MIN_VALUE = 1 - STOP_BIT_MAX_VALUE = 2 + STOP_BIT_MIN_VALUE = 0 + STOP_BIT_MAX_VALUE = 1 REPLY_DELAY_MIN_VALUE = 0 REPLY_DELAY_MAX_VALUE = 1000 @@ -493,3 +493,21 @@ def parity(self, value: int) -> None: msg = f"Invalid parity value: {value}. Must be between 1 and 2." raise ValueError(msg) self._write_register(self.EM511_REGISTER_PARITY, value) + + @stop_bit.setter + def stop_bit(self, value: int) -> None: + """Stop bit. + + 0=1 stop bit (default), 1=2 stop bits; fixed to 1 if parity=Even. + + Args: + value (int): Set stop bit. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If stop bit value is out of range. + """ + if not (self.STOP_BIT_MIN_VALUE <= value <= self.STOP_BIT_MAX_VALUE): + msg = f"Invalid stop bit value: {value}. Must be between 0 and 1." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_STOP_BIT, value) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 05c1544..78af197 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -478,19 +478,19 @@ def test_get_stop_bit() -> None: meter = Em511(1, client) """Test 1: should pass""" - mock_result.registers = [0x2] + mock_result.registers = [0x1] client.read_input_registers.return_value = mock_result value = meter.stop_bit - assert value == 2 + assert value == 1 """Test 2: Should pass.""" - mock_result.registers = [0x1] + mock_result.registers = [0x0] client.read_input_registers.return_value = mock_result value = meter.stop_bit - assert value == 1 + assert value == 0 """Test 3: Should raise exception due to value not in range""" - mock_result.registers = [0x3] + mock_result.registers = [0x2] client.read_input_registers.return_value = mock_result with pytest.raises(ValueError, match="Invalid stop bit value:"): _ = meter.stop_bit @@ -686,3 +686,31 @@ def test_set_parity() -> None: client.write_register.assert_called_once_with(address=8194, value=2, device_id=1) client.write_register.reset_mock() + + +def test_set_stop_bit() -> None: + """Test Set stop bit.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set stop bit""" + mock_result.registers = [8195] + client.write_register.return_value = mock_result + meter.stop_bit = 0 + client.write_register.assert_called_once_with(address=8195, value=0, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set stop bit out of range.""" + with pytest.raises(ValueError, match="Invalid stop bit value:"): + meter.stop_bit = 2 + + """Test 3: Try set stop bit at maximum value.""" + mock_result.registers = [8195] + client.write_register.return_value = mock_result + meter.stop_bit = 1 + client.write_register.assert_called_once_with(address=8195, value=1, device_id=1) + + client.write_register.reset_mock() From 0ec908d8a4ba8fbe2ec0e3a3eae4f14998cb57cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 10:07:27 +0100 Subject: [PATCH 18/37] feat(reply-delay): Add setter to set reply delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 18 ++++++++++++++++++ src/em511/test_em511.py | 26 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 667495c..68a89c8 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -511,3 +511,21 @@ def stop_bit(self, value: int) -> None: msg = f"Invalid stop bit value: {value}. Must be between 0 and 1." raise ValueError(msg) self._write_register(self.EM511_REGISTER_STOP_BIT, value) + + @reply_delay.setter + def reply_delay(self, value: int) -> None: + """Reply delay. + + Range: 0 - 1000 ms (default=0). + + Args: + value (int): Set reply delay. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If reply delay value is out of range. + """ + if not (self.REPLY_DELAY_MIN_VALUE <= value <= self.REPLY_DELAY_MAX_VALUE): + msg = f"Invalid reply delay value: {value}. Must be between 0 and 1000." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_REPLY_DELAY, value) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 78af197..d7088bf 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -714,3 +714,29 @@ def test_set_stop_bit() -> None: client.write_register.assert_called_once_with(address=8195, value=1, device_id=1) client.write_register.reset_mock() + + +def test_set_reply_delay() -> None: + """Test Set reply delay.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set reply delay""" + mock_result.registers = [8196] + client.write_register.return_value = mock_result + meter.reply_delay = 0 + client.write_register.assert_called_once_with(address=8196, value=0, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set reply delay out of range.""" + with pytest.raises(ValueError, match="Invalid reply delay value:"): + meter.reply_delay = 1001 + + """Test 3: Try set reply delay at maximum value.""" + mock_result.registers = [8196] + client.write_register.return_value = mock_result + meter.reply_delay = 1000 + client.write_register.assert_called_once_with(address=8196, value=1000, device_id=1) From 2b2edb2b7d921079f564847a895361e511f7a9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 10:19:27 +0100 Subject: [PATCH 19/37] fix(test_em511): Add new test for all setters to check if ModbusException raises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Did this because all setters claim that they will raise a ModbusException if failed to write to single regiser Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 2 +- src/em511/test_em511.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 68a89c8..ebb36b7 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -107,7 +107,7 @@ def _write_register(self, address: int, value: int) -> None: if result.isError(): msg = ( - "Failed to write to single register." + "Failed to write to single register: " f"device_address={self.device_address} address={address} count={value}" ) raise ModbusException(msg) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index d7088bf..7228382 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import pytest +from pymodbus import ModbusException from em511 import Em511 @@ -579,6 +580,12 @@ def test_set_password() -> None: meter.password = 0 client.write_register.assert_called_once_with(address=4096, value=0, device_id=1) + """Test 5: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.password = 1 + def test_set_device_id() -> None: """Test Set device id.""" @@ -617,6 +624,12 @@ def test_set_device_id() -> None: meter.device_id = 1 client.write_register.assert_called_once_with(address=8192, value=1, device_id=1) + """Test 6: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.device_id = 1 + def test_set_baud_rate() -> None: """Test Set baud rate.""" @@ -655,6 +668,12 @@ def test_set_baud_rate() -> None: meter.baud_rate = 1 client.write_register.assert_called_once_with(address=8193, value=1, device_id=1) + """Test 6: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.baud_rate = 1 + def test_set_parity() -> None: """Test Set parity.""" @@ -687,6 +706,12 @@ def test_set_parity() -> None: client.write_register.reset_mock() + """Test 5: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.parity = 1 + def test_set_stop_bit() -> None: """Test Set stop bit.""" @@ -715,6 +740,12 @@ def test_set_stop_bit() -> None: client.write_register.reset_mock() + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.stop_bit = 1 + def test_set_reply_delay() -> None: """Test Set reply delay.""" @@ -740,3 +771,9 @@ def test_set_reply_delay() -> None: client.write_register.return_value = mock_result meter.reply_delay = 1000 client.write_register.assert_called_once_with(address=8196, value=1000, device_id=1) + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.reply_delay = 1 From 5248397d208d3305f02cc8f01e1e77c5e5d5fa0d Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 10:27:17 -0400 Subject: [PATCH 20/37] feat(hour-counter-part): add getter to reaad Partial run-hour counter also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index ebb36b7..e4f1420 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -47,6 +47,7 @@ class Em511: EM511_REGISTER_KWH_PARTIAL = 0x14 EM511_REGISTER_HOUR_COUNTER = 0x2C EM511_REGISTER_LIFETIME_COUNTER = 0x30 + EM511_REGISTER_HOUR_COUNTER_PART = 0x36 EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 @@ -294,6 +295,21 @@ def lifetime_counter(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_LIFETIME_COUNTER)) / self.SCALE_100 return round(value, 2) + @property + def hour_counter_part(self) -> Decimal: + """Partial run-hour counter (h). + + Returns: + Decimal: partial operating tim in hours value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_HOUR_COUNTER_PART, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_HOUR_COUNTER_PART)) / self.SCALE_100 + return round(value, 2) + @property def password(self) -> int: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 7228382..49c34f9 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -331,6 +331,38 @@ def test_get_lifetime_counter() -> None: _ = meter.lifetime_counter +def test_get_life_time_counter() -> None: + """Test Get hour_counter_partial.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.hour_counter_part + assert value == Decimal("105.0") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.hour_counter_part + assert value == Decimal("23000.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.hour_counter_part + + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.hour_counter_part + + def test_get_password() -> None: """Test Get password.""" client = MagicMock() From dbd2e5cc80c6c21a68c0aabee7a77fdca7a06f2b Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 10:43:39 -0400 Subject: [PATCH 21/37] feat(A-dmd): add getter to read current demand peak also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index e4f1420..e06f86b 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -38,6 +38,7 @@ class Em511: EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 + EM511_REGISTER_A_DMD = 0x3A EM511_REGISTER_W = 0x4 EM511_REGISTER_W_DMD = 0xA EM511_REGISTER_W_DMD_PEAK = 0xC @@ -175,6 +176,21 @@ def A(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_A)) / self.SCALE_1000 return round(value, 3) + @property + def A_dmd(self) -> Decimal: + """Current demand (A). + + Returns: + Decimal: Current demand ampere value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_A_DMD, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_A_DMD)) / self.SCALE_1000 + return round(value, 3) + @property def W(self) -> Decimal: """Power (W). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 49c34f9..8f90980 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -75,6 +75,38 @@ def test_get_A() -> None: _ = meter.A +def test_get_A_dmd() -> None: + """Test Get a_dmd.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.A_dmd + assert value == Decimal("10.5") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.A_dmd + assert value == Decimal("2300.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.A_dmd + + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.A_dmd + + def test_get_W() -> None: """Test Get w.""" client = MagicMock() From 140ec038ea30b585c02af45dabb395bac2c1606d Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 11:31:25 -0400 Subject: [PATCH 22/37] feat(A-dmd-peak): add getter to read current demand peak ampere value also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 16 ++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index e06f86b..616633e 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -39,6 +39,7 @@ class Em511: EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 EM511_REGISTER_A_DMD = 0x3A + EM511_REGISTER_A_DMD_PEAK = 0x3C EM511_REGISTER_W = 0x4 EM511_REGISTER_W_DMD = 0xA EM511_REGISTER_W_DMD_PEAK = 0xC @@ -191,6 +192,21 @@ def A_dmd(self) -> Decimal: value = Decimal(self._unpack(regs, self.EM511_REGISTER_A_DMD)) / self.SCALE_1000 return round(value, 3) + @property + def A_dmd_peak(self) -> Decimal: + """Current demand peak (A). + + Returns: + Decimal: Current demand peak, ampere value. + + Raises: + ValueError: If input is at max value or above. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_A_DMD_PEAK, self.INT32_REG_COUNT) + value = Decimal(self._unpack(regs, self.EM511_REGISTER_A_DMD_PEAK)) / self.SCALE_1000 + return round(value, 3) + @property def W(self) -> Decimal: """Power (W). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 8f90980..d757e5c 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -107,6 +107,38 @@ def test_get_A_dmd() -> None: _ = meter.A_dmd +def test_get_A_dmd_peak() -> None: + """Test Get a_dmd_peak.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x2904, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.A_dmd_peak + assert value == Decimal("10.5") + + """Test 2: Should pass.""" + mock_result.registers = [0x1860, 0x0023] + client.read_input_registers.return_value = mock_result + value = meter.A_dmd_peak + assert value == Decimal("2300.0") + + """Test 3: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.A_dmd_peak + + """Test 4: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.A_dmd_peak + + def test_get_W() -> None: """Test Get w.""" client = MagicMock() From 4efafd3770e02a050c49c62093898f4aab3a8ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 11:42:08 +0100 Subject: [PATCH 23/37] feat(reset-tot-energy-and-run-hour-counter): Add reset to reset totall energy run hour counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 11 +++++++++++ src/em511/test_em511.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 616633e..e8e4b3f 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -55,6 +55,7 @@ class Em511: EM511_REGISTER_PARITY = 0x2002 EM511_REGISTER_STOP_BIT = 0x2003 EM511_REGISTER_REPLY_DELAY = 0x2004 + EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER = 0x4003 SCALE_10 = 10 SCALE_100 = 100 @@ -577,3 +578,13 @@ def reply_delay(self, value: int) -> None: msg = f"Invalid reply delay value: {value}. Must be between 0 and 1000." raise ValueError(msg) self._write_register(self.EM511_REGISTER_REPLY_DELAY, value) + + def reset_tot_energy_and_run_hour_counter(self) -> None: + """Reset total energy + total run hour counters (excluding lifetime). + + Writes 1 to execute. + + Raises: + ModbusException: If failed to write to single register. + """ + self._write_register(self.EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER, 1) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index d757e5c..de373fc 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -873,3 +873,25 @@ def test_set_reply_delay() -> None: client.write_register.return_value = mock_result with pytest.raises(ModbusException, match="Failed to write to single register:"): meter.reply_delay = 1 + + +def test_reset_tot_energy_and_run_hour_counter() -> None: + """Test Reset total energy and run hour counter.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Reset""" + mock_result.registers = [16387] + client.write_register.return_value = mock_result + meter.reset_tot_energy_and_run_hour_counter() + client.write_register.assert_called_once_with(address=16387, value=1, device_id=1) + + client.write_register.reset_mock() + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.reset_tot_energy_and_run_hour_counter() From 69c7a8d69726e39d3308260bcea21e87f92a8d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 11:51:05 +0100 Subject: [PATCH 24/37] feat(reset-partial-energy-and-hour-counter): Add reset to reset partial energy and hour counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 11 +++++++++++ src/em511/test_em511.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index e8e4b3f..1852652 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -56,6 +56,7 @@ class Em511: EM511_REGISTER_STOP_BIT = 0x2003 EM511_REGISTER_REPLY_DELAY = 0x2004 EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER = 0x4003 + EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 SCALE_10 = 10 SCALE_100 = 100 @@ -588,3 +589,13 @@ def reset_tot_energy_and_run_hour_counter(self) -> None: ModbusException: If failed to write to single register. """ self._write_register(self.EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER, 1) + + def reset_partial_energy_and_hour_counter(self) -> None: + """Reset partial energy + partial run hour counters. + + Writes 1 to execute. + + Raises: + ModbusException: If failed to write to single register. + """ + self._write_register(self.EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER, 1) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index de373fc..7a0d286 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -895,3 +895,25 @@ def test_reset_tot_energy_and_run_hour_counter() -> None: client.write_register.return_value = mock_result with pytest.raises(ModbusException, match="Failed to write to single register:"): meter.reset_tot_energy_and_run_hour_counter() + + +def test_reset_partial_energy_and_partial_hour() -> None: + """Test Reset partial energy + partial run hour counters.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Reset""" + mock_result.registers = [16388] + client.write_register.return_value = mock_result + meter.reset_partial_energy_and_hour_counter() + client.write_register.assert_called_once_with(address=16388, value=1, device_id=1) + + client.write_register.reset_mock() + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.reset_partial_energy_and_hour_counter() From d929ed6bed7401055545b8455224c3041ba5b724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 13:56:01 +0100 Subject: [PATCH 25/37] feat(reset-to-factory-settings): Add reset unction to reset back to factory settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aslo add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 12 ++++++++++++ src/em511/test_em511.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 1852652..8eb19da 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -57,6 +57,7 @@ class Em511: EM511_REGISTER_REPLY_DELAY = 0x2004 EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER = 0x4003 EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 + EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 SCALE_10 = 10 SCALE_100 = 100 @@ -599,3 +600,14 @@ def reset_partial_energy_and_hour_counter(self) -> None: ModbusException: If failed to write to single register. """ self._write_register(self.EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER, 1) + + def reset_to_factory_settings(self) -> None: + """Factory Restore (Default settings). + + Write 0x0A0A=2570, then within 1s write 0xC1A0=49568 to trigger reset. + + Raises: + ModbusException: If failed to write to single register. + """ + self._write_register(self.EM511_REGISTER_RESET_TO_FACTORY_SETTINGS, 0x0A0A) + self._write_register(self.EM511_REGISTER_RESET_TO_FACTORY_SETTINGS, 0xC1A0) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 7a0d286..81cd42c 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -3,7 +3,7 @@ """Test file for driver.""" from decimal import Decimal -from unittest.mock import MagicMock +from unittest.mock import MagicMock, call import pytest from pymodbus import ModbusException @@ -897,8 +897,8 @@ def test_reset_tot_energy_and_run_hour_counter() -> None: meter.reset_tot_energy_and_run_hour_counter() -def test_reset_partial_energy_and_partial_hour() -> None: - """Test Reset partial energy + partial run hour counters.""" +def test_reset_partial_energy_and_hour_counter() -> None: + """Test Reset partial energy + partial hour counters.""" client = MagicMock() mock_result = MagicMock() mock_result.isError.return_value = False @@ -917,3 +917,27 @@ def test_reset_partial_energy_and_partial_hour() -> None: client.write_register.return_value = mock_result with pytest.raises(ModbusException, match="Failed to write to single register:"): meter.reset_partial_energy_and_hour_counter() + + +def test_reset_to_factory_settings() -> None: + """Test Reset DMD and DMD max values.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + client.write_register.return_value = mock_result + meter = Em511(1, client) + + """Test 1: Both writes occur""" + meter.reset_to_factory_settings() + expected_calls = [ + call(address=16416, value=0x0A0A, device_id=1), + call(address=16416, value=0xC1A0, device_id=1), + ] + assert client.write_register.call_args_list == expected_calls + assert client.write_register.call_count == 2 + + """Test 3: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.reset_to_factory_settings() From 850111f1425d05523f9579272e6518061bea3057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 13:59:51 +0100 Subject: [PATCH 26/37] feat(reset-dmd-and-max-dmd): Add reset function to reset demand and max demand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 11 +++++++++++ src/em511/test_em511.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 8eb19da..83e06d2 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -57,6 +57,7 @@ class Em511: EM511_REGISTER_REPLY_DELAY = 0x2004 EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER = 0x4003 EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 + EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005 EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 SCALE_10 = 10 @@ -601,6 +602,16 @@ def reset_partial_energy_and_hour_counter(self) -> None: """ self._write_register(self.EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER, 1) + def reset_dmd_and_dmd_max(self) -> None: + """Reset DMD and DMD max values. + + Write 1 to execute. + + Raises: + ModbusException: If failed to write to single register. + """ + self._write_register(self.EM511_REGISTER_RESET_DMD_AND_DMD_MAX, 1) + def reset_to_factory_settings(self) -> None: """Factory Restore (Default settings). diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 81cd42c..be28373 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -919,6 +919,28 @@ def test_reset_partial_energy_and_hour_counter() -> None: meter.reset_partial_energy_and_hour_counter() +def test_reset_dmd_and_dmd_max() -> None: + """Test Reset DMD and DMD max values..""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Reset""" + mock_result.registers = [16389] + client.write_register.return_value = mock_result + meter.reset_dmd_and_dmd_max() + client.write_register.assert_called_once_with(address=16389, value=1, device_id=1) + + client.write_register.reset_mock() + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.reset_dmd_and_dmd_max() + + def test_reset_to_factory_settings() -> None: """Test Reset DMD and DMD max values.""" client = MagicMock() From 3a82b49cdb5100400f10b4cf27291f503e160ab9 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 14:15:11 -0400 Subject: [PATCH 27/37] feat(dmd-integration-time): add getter to read demand integration time setting also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 25 +++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 83e06d2..aa4a579 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -35,6 +35,8 @@ class Em511: STOP_BIT_MAX_VALUE = 1 REPLY_DELAY_MIN_VALUE = 0 REPLY_DELAY_MAX_VALUE = 1000 + DMD_TIME_MIN_VALUE = 0 + DMD_TIME_MAX_VALUE = 6 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -59,6 +61,7 @@ class Em511: EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005 EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 + EM511_REGISTER_DMD_INTEGRATION_TIME = 0x1010 SCALE_10 = 10 SCALE_100 = 100 @@ -365,6 +368,28 @@ def password(self) -> int: raise ValueError(msg) return value + @property + def dmd_integration_time(self) -> int: + """Demand integration time. + + 0=1min, 1=5min, 2=10min, 3=15min, + 4=20min, 5=30min, 6=60min (default=3). + + Returns: + int: Current demand integration time. + + Raises: + ValueError: If input is at max value or above. + ValueError: If demand integration time is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_DMD_INTEGRATION_TIME, self.INT32_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_DMD_INTEGRATION_TIME) + if not (self.DMD_TIME_MIN_VALUE <= value <= self.DMD_TIME_MAX_VALUE): + msg = f"Invalid demand integration time value: {value}. Must be between 0 and 6." + raise ValueError(msg) + return value + @property def device_id(self) -> int: """Device address. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index be28373..7424c10 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -453,6 +453,44 @@ def test_get_password() -> None: _ = meter.password +def test_get_dmd_integration_time() -> None: + """Test Get dmd_integration_time.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x0006, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.dmd_integration_time + assert value == int("6") + + """Test 2: Should pass.""" + mock_result.registers = [0x0000, 0x0000] + client.read_input_registers.return_value = mock_result + value = meter.dmd_integration_time + assert value == int("0") + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x0007, 0x0000] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid demand integration time value: "): + _ = meter.dmd_integration_time + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.dmd_integration_time + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0xFFFF, 0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 32-bit register: "): + _ = meter.dmd_integration_time + + def test_get_device_id() -> None: """Test Get device id/address.""" client = MagicMock() From d734504d5f8e883a70132071da9495e1b85d3e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 14:40:14 +0100 Subject: [PATCH 28/37] feat(dmd-integration-time): Add setter to set demand integration time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 21 ++++++++++++++++++++- src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index aa4a579..0aca775 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -52,6 +52,7 @@ class Em511: EM511_REGISTER_HOUR_COUNTER = 0x2C EM511_REGISTER_LIFETIME_COUNTER = 0x30 EM511_REGISTER_HOUR_COUNTER_PART = 0x36 + EM511_REGISTER_DMD_INTEGRATION_TIME = 0x1010 EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 @@ -61,7 +62,6 @@ class Em511: EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005 EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 - EM511_REGISTER_DMD_INTEGRATION_TIME = 0x1010 SCALE_10 = 10 SCALE_100 = 100 @@ -496,6 +496,25 @@ def reply_delay(self) -> int: raise ValueError(msg) return value + @dmd_integration_time.setter + def dmd_integration_time(self, value: int) -> None: + """Demand integration time. + + Value: 0=1 min, 1=5 min, 2=10 min, 3=15 min(default) + 4=20 min, 5=30 min, 6=30 min. + + Args: + value (int): Set demand integration time. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If demand integration time value is out of range. + """ + if not (self.DMD_TIME_MIN_VALUE <= value <= self.DMD_TIME_MAX_VALUE): + msg = f"Invalid demand integration time value: {value}. Must be between 0 and 6." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_DMD_INTEGRATION_TIME, value) + @password.setter def password(self, value: int) -> None: """Password. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 7424c10..6ad082a 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -681,6 +681,38 @@ def test_get_reply_delay() -> None: _ = meter.reply_delay +def test_set_dmd_integration_time() -> None: + """Test Set demand integration time.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set demand integration time""" + mock_result.registers = [4112] + client.write_register.return_value = mock_result + meter.dmd_integration_time = 0 + client.write_register.assert_called_once_with(address=4112, value=0, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set demand integration time out of range.""" + with pytest.raises(ValueError, match="Invalid demand integration time value:"): + meter.dmd_integration_time = 7 + + """Test 3: Try set demand integration time at maximum value.""" + mock_result.registers = [4112] + client.write_register.return_value = mock_result + meter.dmd_integration_time = 6 + client.write_register.assert_called_once_with(address=4112, value=6, device_id=1) + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.dmd_integration_time = 1 + + def test_set_password() -> None: """Test Set Password.""" client = MagicMock() From b95db6dd6d2d01f0496c1580f01cab2189bf3807 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 14:45:37 -0400 Subject: [PATCH 29/37] feat(alarm-state): add getter to read alram state also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 25 +++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 0aca775..02c833b 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -37,6 +37,8 @@ class Em511: REPLY_DELAY_MAX_VALUE = 1000 DMD_TIME_MIN_VALUE = 0 DMD_TIME_MAX_VALUE = 6 + ALARM_STATE_MIN_VALUE = 0 + ALARM_STATE_MAX_VALUE = 1 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -62,6 +64,7 @@ class Em511: EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004 EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005 EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 + EM511_REGISTER_ALARM_STATE = 0x1014 SCALE_10 = 10 SCALE_100 = 100 @@ -368,6 +371,28 @@ def password(self) -> int: raise ValueError(msg) return value + @property + def alarm_state(self) -> int: + """Alarm state. + + 0=Disabled, Default + 1=Enabled + + Returns: + int: Current alarm state. + + Raises: + ValueError: If input is at max value or above. + ValueError: If alarm state is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_ALARM_STATE, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_ALARM_STATE) + if not (self.ALARM_STATE_MIN_VALUE <= value <= self.ALARM_STATE_MAX_VALUE): + msg = f"Invalid alarm state value: {value}. Must be between 0 and 1." + raise ValueError(msg) + return value + @property def dmd_integration_time(self) -> int: """Demand integration time. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 6ad082a..111d0e8 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -453,6 +453,44 @@ def test_get_password() -> None: _ = meter.password +def test_get_alarm_state() -> None: + """Test Get alarm_state.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x0001] + client.read_input_registers.return_value = mock_result + value = meter.alarm_state + assert value == int("1") + + """Test 2: Should pass.""" + mock_result.registers = [0x0000] + client.read_input_registers.return_value = mock_result + value = meter.alarm_state + assert value == int("0") + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x0002] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid alarm state value: "): + _ = meter.alarm_state + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.alarm_state + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.alarm_state + + def test_get_dmd_integration_time() -> None: """Test Get dmd_integration_time.""" client = MagicMock() From 0f1a74e6b70cd868151d9a4a336c8c8b237e0fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 14:53:38 +0100 Subject: [PATCH 30/37] feat(alarm-state): Add setter to set alarm state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 19 +++++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 02c833b..c9999d3 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -559,6 +559,25 @@ def password(self, value: int) -> None: raise ValueError(msg) self._write_register(self.EM511_REGISTER_PASSWORD, value) + @alarm_state.setter + def alarm_state(self, value: int) -> None: + """Alarm state. + + Value: 0=Disabled (default) + 1=Enabled + + Args: + value (int): Set alarm state. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If alarm state value is out of range. + """ + if not (self.ALARM_STATE_MIN_VALUE <= value <= self.ALARM_STATE_MAX_VALUE): + msg = f"Invalid alarm state value: {value}. Must be between 0 and 1." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_ALARM_STATE, value) + @device_id.setter def device_id(self, value: int) -> None: """Device id. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 111d0e8..bf1031c 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -791,6 +791,38 @@ def test_set_password() -> None: meter.password = 1 +def test_set_alarm_state() -> None: + """Test Set alarm state.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set alarm state""" + mock_result.registers = [4116] + client.write_register.return_value = mock_result + meter.alarm_state = 0 + client.write_register.assert_called_once_with(address=4116, value=0, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set alarm state out of range.""" + with pytest.raises(ValueError, match="Invalid alarm state value:"): + meter.alarm_state = 2 + + """Test 3: Try set alarm state at maximum value.""" + mock_result.registers = [4116] + client.write_register.return_value = mock_result + meter.alarm_state = 1 + client.write_register.assert_called_once_with(address=4116, value=1, device_id=1) + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.alarm_state = 1 + + def test_set_device_id() -> None: """Test Set device id.""" client = MagicMock() From cd43ebfcdaca403211ff3f24f13d7516e6628ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 15:08:09 +0100 Subject: [PATCH 31/37] feat(registers): Add register and max & min values for alarm state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index c9999d3..fa5365e 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -39,6 +39,8 @@ class Em511: DMD_TIME_MAX_VALUE = 6 ALARM_STATE_MIN_VALUE = 0 ALARM_STATE_MAX_VALUE = 1 + ALARM_MODE_MAX_VALUE = 6 + ALARM_MODE_MIN_VALUE = 1 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -55,6 +57,7 @@ class Em511: EM511_REGISTER_LIFETIME_COUNTER = 0x30 EM511_REGISTER_HOUR_COUNTER_PART = 0x36 EM511_REGISTER_DMD_INTEGRATION_TIME = 0x1010 + EM511_REGISTER_ALARM_MODE = 0x1015 EM511_REGISTER_DEVICE_ID = 0x2000 EM511_REGISTER_BAUD_RATE = 0x2001 EM511_REGISTER_PARITY = 0x2002 From 5671f42c5b2bba68694f56811a0e6cd9d7098526 Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 15:14:40 -0400 Subject: [PATCH 32/37] feat(alarm-mode): add getter to read alarm mode also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 28 +++++++++++++++++++++++++++- src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index fa5365e..357bcfa 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -392,7 +392,33 @@ def alarm_state(self) -> int: regs = self._read_input_registers(self.EM511_REGISTER_ALARM_STATE, self.INT16_REG_COUNT) value = self._unpack(regs, self.EM511_REGISTER_ALARM_STATE) if not (self.ALARM_STATE_MIN_VALUE <= value <= self.ALARM_STATE_MAX_VALUE): - msg = f"Invalid alarm state value: {value}. Must be between 0 and 1." + msg = f"Invalid alarm state value: {value}. Must be between 1 and 6." + raise ValueError(msg) + return value + + @property + def alarm_mode(self) -> int: + """Alarm mode. + + 1=Active Power(kW) + 2=Apparent Power (kVA) (default) + 3=Reactive Power (kVAR) + 4=Power Factor + 5=Current (A) + 6=Voltages (V) + + Returns: + int: Current alarm mode. + + Raises: + ValueError: If input is at max value or above. + ValueError: If alarm state is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_ALARM_MODE, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_ALARM_MODE) + if not (self.ALARM_MODE_MIN_VALUE <= value <= self.ALARM_MODE_MAX_VALUE): + msg = f"Invalid alarm mode value: {value}. Must be between 1 and 6." raise ValueError(msg) return value diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index bf1031c..73dad18 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -823,6 +823,44 @@ def test_set_alarm_state() -> None: meter.alarm_state = 1 +def test_get_alarm_mode() -> None: + """Test Get alarm_mode.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x0006] + client.read_input_registers.return_value = mock_result + value = meter.alarm_mode + assert value == int("6") + + """Test 2: Should pass.""" + mock_result.registers = [0x0001] + client.read_input_registers.return_value = mock_result + value = meter.alarm_mode + assert value == int("1") + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x0007] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid alarm mode value: "): + _ = meter.alarm_mode + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.alarm_mode + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.alarm_mode + + def test_set_device_id() -> None: """Test Set device id.""" client = MagicMock() From a6bfeccb91a3966905d4343b368950da84727d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 15:20:55 +0100 Subject: [PATCH 33/37] feat(alarm-mode): Add setter to set alarm mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 21 +++++++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 357bcfa..60cc04f 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -607,6 +607,27 @@ def alarm_state(self, value: int) -> None: raise ValueError(msg) self._write_register(self.EM511_REGISTER_ALARM_STATE, value) + @alarm_mode.setter + def alarm_mode(self, value: int) -> None: + """Alarm mode. + + Value: + 1=Active Power [kW], 2=Apparent Power [kVA], + 3=Reactive Power [kVAR], 4=Power Factor, + 5=Currents [A], 6=Voltages [V] (default=1) + + Args: + value (int): Set alarm mode. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If alarm mode value is out of range. + """ + if not (self.ALARM_MODE_MIN_VALUE <= value <= self.ALARM_MODE_MAX_VALUE): + msg = f"Invalid alarm mode value: {value}. Must be between 1 and 6." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_ALARM_MODE, value) + @device_id.setter def device_id(self, value: int) -> None: """Device id. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 73dad18..10b611e 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -823,6 +823,38 @@ def test_set_alarm_state() -> None: meter.alarm_state = 1 +def test_set_alarm_mode() -> None: + """Test Set alarm mode.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set alarm mode""" + mock_result.registers = [4117] + client.write_register.return_value = mock_result + meter.alarm_mode = 1 + client.write_register.assert_called_once_with(address=4117, value=1, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set alarm mode out of range.""" + with pytest.raises(ValueError, match="Invalid alarm mode value:"): + meter.alarm_mode = 7 + + """Test 3: Try set alarm mode at maximum value.""" + mock_result.registers = [4117] + client.write_register.return_value = mock_result + meter.alarm_mode = 6 + client.write_register.assert_called_once_with(address=4117, value=6, device_id=1) + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.alarm_mode = 1 + + def test_get_alarm_mode() -> None: """Test Get alarm_mode.""" client = MagicMock() From e636fde86f5222a7d219eaff95ee58de6afdd398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 15:23:11 +0100 Subject: [PATCH 34/37] fix(test-em511): Change places on two tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit did this because thests where in worng order Signed-off-by: Bobo Bäck Engström --- src/em511/test_em511.py | 76 ++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 10b611e..f93856c 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -491,6 +491,44 @@ def test_get_alarm_state() -> None: _ = meter.alarm_state +def test_get_alarm_mode() -> None: + """Test Get alarm_mode.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x0006] + client.read_input_registers.return_value = mock_result + value = meter.alarm_mode + assert value == int("6") + + """Test 2: Should pass.""" + mock_result.registers = [0x0001] + client.read_input_registers.return_value = mock_result + value = meter.alarm_mode + assert value == int("1") + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x0007] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid alarm mode value: "): + _ = meter.alarm_mode + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.alarm_mode + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.alarm_mode + + def test_get_dmd_integration_time() -> None: """Test Get dmd_integration_time.""" client = MagicMock() @@ -855,44 +893,6 @@ def test_set_alarm_mode() -> None: meter.alarm_mode = 1 -def test_get_alarm_mode() -> None: - """Test Get alarm_mode.""" - client = MagicMock() - mock_result = MagicMock() - mock_result.isError.return_value = False - meter = Em511(1, client) - - """Test 1: should pass""" - mock_result.registers = [0x0006] - client.read_input_registers.return_value = mock_result - value = meter.alarm_mode - assert value == int("6") - - """Test 2: Should pass.""" - mock_result.registers = [0x0001] - client.read_input_registers.return_value = mock_result - value = meter.alarm_mode - assert value == int("1") - - """Test 3: Should raise exception due to value not in range""" - mock_result.registers = [0x0007] - client.read_input_registers.return_value = mock_result - with pytest.raises(ValueError, match="Invalid alarm mode value: "): - _ = meter.alarm_mode - - """Test 4: Should raise exception due to more registers in use than allowed.""" - mock_result.registers = [0x1860, 0x0023, 0x4244] - client.read_input_registers.return_value = mock_result - with pytest.raises(ValueError, match="Unexpected register count:"): - _ = meter.alarm_mode - - """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 32-bit register.""" - mock_result.registers = [0x7FFF] - client.read_input_registers.return_value = mock_result - with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): - _ = meter.alarm_mode - - def test_set_device_id() -> None: """Test Set device id.""" client = MagicMock() From 1bd833ad3d5149a1b4523dd4e30077ec2414fadc Mon Sep 17 00:00:00 2001 From: Milad Makdesi Date: Wed, 29 Oct 2025 15:29:23 -0400 Subject: [PATCH 35/37] feat(alram-delay): add getter to read alarm mode delay also added valid tests Signed-off-by: Milad Makdesi --- src/em511/em511.py | 25 +++++++++++++++++++++++++ src/em511/test_em511.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 60cc04f..84fd4b5 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -41,6 +41,8 @@ class Em511: ALARM_STATE_MAX_VALUE = 1 ALARM_MODE_MAX_VALUE = 6 ALARM_MODE_MIN_VALUE = 1 + ALARM_DELAY_MIN_VALUE = 0 + ALARM_DELAY_MAX_VALUE = 3600 EM511_REGISTER_V = 0x0 EM511_REGISTER_A = 0x2 @@ -68,6 +70,7 @@ class Em511: EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005 EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020 EM511_REGISTER_ALARM_STATE = 0x1014 + EM511_REGISTER_ALARM_DELAY = 0x101A SCALE_10 = 10 SCALE_100 = 100 @@ -422,6 +425,28 @@ def alarm_mode(self) -> int: raise ValueError(msg) return value + @property + def alarm_delay(self) -> int: + """Alarm mode. + + Value: Min=0 (default) + MAx=3600 + + Returns: + int: Current alarm delay setting. + + Raises: + ValueError: If input is at max value or above. + ValueError: If alarm state is out of range. + ModbusException: If failed to read input register. + """ + regs = self._read_input_registers(self.EM511_REGISTER_ALARM_DELAY, self.INT16_REG_COUNT) + value = self._unpack(regs, self.EM511_REGISTER_ALARM_DELAY) + if not (self.ALARM_DELAY_MIN_VALUE <= value <= self.ALARM_DELAY_MAX_VALUE): + msg = f"Invalid alarm delay value: {value}. Must be between 0 and 3600." + raise ValueError(msg) + return value + @property def dmd_integration_time(self) -> int: """Demand integration time. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index f93856c..60985b5 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -529,6 +529,44 @@ def test_get_alarm_mode() -> None: _ = meter.alarm_mode +def test_get_alarm_delay() -> None: + """Test Get alarm_delay.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: should pass""" + mock_result.registers = [0x0E10] + client.read_input_registers.return_value = mock_result + value = meter.alarm_delay + assert value == int("3600") + + """Test 2: Should pass.""" + mock_result.registers = [0x0000] + client.read_input_registers.return_value = mock_result + value = meter.alarm_delay + assert value == int("0") + + """Test 3: Should raise exception due to value not in range""" + mock_result.registers = [0x0E11] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Invalid alarm delay value: "): + _ = meter.alarm_delay + + """Test 4: Should raise exception due to more registers in use than allowed.""" + mock_result.registers = [0x1860, 0x0023, 0x4244] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Unexpected register count:"): + _ = meter.alarm_delay + + """Test 5: Should raise exception if input value exceeds maximum value, display shows 'EEE', 16-bit register.""" + mock_result.registers = [0x7FFF] + client.read_input_registers.return_value = mock_result + with pytest.raises(ValueError, match="Input overflow EEE for 16-bit register: "): + _ = meter.alarm_delay + + def test_get_dmd_integration_time() -> None: """Test Get dmd_integration_time.""" client = MagicMock() From c4f50dc404fcb0762ed33323960b059d0ceed665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 15:31:44 +0100 Subject: [PATCH 36/37] feat(alarm-delay): Add setter to set alarm delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add valid tests Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 19 +++++++++++++++++++ src/em511/test_em511.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/em511/em511.py b/src/em511/em511.py index 84fd4b5..830731d 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -653,6 +653,25 @@ def alarm_mode(self, value: int) -> None: raise ValueError(msg) self._write_register(self.EM511_REGISTER_ALARM_MODE, value) + @alarm_delay.setter + def alarm_delay(self, value: int) -> None: + """Alarm delay. + + Value: + Range 0 - 3600 s(Default=0). + + Args: + value (int): Set Alarm Delay. + + Raises: + ModbusException: If failed to write to single register. + ValueError: If alarm delay value is out of range. + """ + if not (self.ALARM_DELAY_MIN_VALUE <= value <= self.ALARM_DELAY_MAX_VALUE): + msg = f"Invalid alarm delay value: {value}. Must be between 0 and 3600." + raise ValueError(msg) + self._write_register(self.EM511_REGISTER_ALARM_DELAY, value) + @device_id.setter def device_id(self, value: int) -> None: """Device id. diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 60985b5..888b2e7 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -931,6 +931,38 @@ def test_set_alarm_mode() -> None: meter.alarm_mode = 1 +def test_set_alarm_delay() -> None: + """Test Set alarm delay.""" + client = MagicMock() + mock_result = MagicMock() + mock_result.isError.return_value = False + meter = Em511(1, client) + + """Test 1: Set alarm delay""" + mock_result.registers = [4122] + client.write_register.return_value = mock_result + meter.alarm_delay = 0 + client.write_register.assert_called_once_with(address=4122, value=0, device_id=1) + + client.write_register.reset_mock() + + """Test 2: Try set alarm delay out of range.""" + with pytest.raises(ValueError, match="Invalid alarm delay value:"): + meter.alarm_delay = 3601 + + """Test 3: Try set alarm delay at maximum value.""" + mock_result.registers = [4122] + client.write_register.return_value = mock_result + meter.alarm_delay = 3600 + client.write_register.assert_called_once_with(address=4122, value=3600, device_id=1) + + """Test 4: Should raise exception due to failed writing to single register.""" + mock_result.isError.return_value = True + client.write_register.return_value = mock_result + with pytest.raises(ModbusException, match="Failed to write to single register:"): + meter.alarm_delay = 1 + + def test_set_device_id() -> None: """Test Set device id.""" client = MagicMock() From 3d130841d49e44fa4441129fafec0d8a1936693b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bobo=20B=C3=A4ck=20Engstr=C3=B6m?= Date: Wed, 29 Oct 2025 15:38:27 +0100 Subject: [PATCH 37/37] fix(test-em511): Change register value from decimal to HEX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Did this to be more consistent Signed-off-by: Bobo Bäck Engström --- src/em511/em511.py | 26 +++++------ src/em511/test_em511.py | 100 ++++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/em511/em511.py b/src/em511/em511.py index 830731d..bd2ab35 100644 --- a/src/em511/em511.py +++ b/src/em511/em511.py @@ -44,20 +44,20 @@ class Em511: ALARM_DELAY_MIN_VALUE = 0 ALARM_DELAY_MAX_VALUE = 3600 - EM511_REGISTER_V = 0x0 - EM511_REGISTER_A = 0x2 - EM511_REGISTER_A_DMD = 0x3A - EM511_REGISTER_A_DMD_PEAK = 0x3C - EM511_REGISTER_W = 0x4 - EM511_REGISTER_W_DMD = 0xA - EM511_REGISTER_W_DMD_PEAK = 0xC - EM511_REGISTER_HZ = 0xF + EM511_REGISTER_V = 0x0000 + EM511_REGISTER_A = 0x0002 + EM511_REGISTER_A_DMD = 0x003A + EM511_REGISTER_A_DMD_PEAK = 0x003C + EM511_REGISTER_W = 0x0004 + EM511_REGISTER_W_DMD = 0x000A + EM511_REGISTER_W_DMD_PEAK = 0x000C + EM511_REGISTER_HZ = 0x000F EM511_REGISTER_PASSWORD = 0x1000 - EM511_REGISTER_KWH_TOT = 0x10 - EM511_REGISTER_KWH_PARTIAL = 0x14 - EM511_REGISTER_HOUR_COUNTER = 0x2C - EM511_REGISTER_LIFETIME_COUNTER = 0x30 - EM511_REGISTER_HOUR_COUNTER_PART = 0x36 + EM511_REGISTER_KWH_TOT = 0x0010 + EM511_REGISTER_KWH_PARTIAL = 0x0014 + EM511_REGISTER_HOUR_COUNTER = 0x002C + EM511_REGISTER_LIFETIME_COUNTER = 0x0030 + EM511_REGISTER_HOUR_COUNTER_PART = 0x0036 EM511_REGISTER_DMD_INTEGRATION_TIME = 0x1010 EM511_REGISTER_ALARM_MODE = 0x1015 EM511_REGISTER_DEVICE_ID = 0x2000 diff --git a/src/em511/test_em511.py b/src/em511/test_em511.py index 888b2e7..85743fe 100644 --- a/src/em511/test_em511.py +++ b/src/em511/test_em511.py @@ -835,10 +835,10 @@ def test_set_password() -> None: meter = Em511(1, client) """Test 1: Set password""" - mock_result.registers = [4096] + mock_result.registers = [0x1000] client.write_register.return_value = mock_result meter.password = 1236 - client.write_register.assert_called_once_with(address=4096, value=1236, device_id=1) + client.write_register.assert_called_once_with(address=0x1000, value=1236, device_id=1) client.write_register.reset_mock() @@ -847,18 +847,18 @@ def test_set_password() -> None: meter.password = 12345 """Test 3: Try set password at maximum value.""" - mock_result.registers = [4096] + mock_result.registers = [0x1000] client.write_register.return_value = mock_result meter.password = 9999 - client.write_register.assert_called_once_with(address=4096, value=9999, device_id=1) + client.write_register.assert_called_once_with(address=0x1000, value=9999, device_id=1) client.write_register.reset_mock() """Test 4: Try set password at lowest value.""" - mock_result.registers = [4096] + mock_result.registers = [0x1000] client.write_register.return_value = mock_result meter.password = 0 - client.write_register.assert_called_once_with(address=4096, value=0, device_id=1) + client.write_register.assert_called_once_with(address=0x1000, value=0, device_id=1) """Test 5: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -875,10 +875,10 @@ def test_set_alarm_state() -> None: meter = Em511(1, client) """Test 1: Set alarm state""" - mock_result.registers = [4116] + mock_result.registers = [0x1014] client.write_register.return_value = mock_result meter.alarm_state = 0 - client.write_register.assert_called_once_with(address=4116, value=0, device_id=1) + client.write_register.assert_called_once_with(address=0x1014, value=0, device_id=1) client.write_register.reset_mock() @@ -887,10 +887,10 @@ def test_set_alarm_state() -> None: meter.alarm_state = 2 """Test 3: Try set alarm state at maximum value.""" - mock_result.registers = [4116] + mock_result.registers = [0x1014] client.write_register.return_value = mock_result meter.alarm_state = 1 - client.write_register.assert_called_once_with(address=4116, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x1014, value=1, device_id=1) """Test 4: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -907,10 +907,10 @@ def test_set_alarm_mode() -> None: meter = Em511(1, client) """Test 1: Set alarm mode""" - mock_result.registers = [4117] + mock_result.registers = [0x1015] client.write_register.return_value = mock_result meter.alarm_mode = 1 - client.write_register.assert_called_once_with(address=4117, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x1015, value=1, device_id=1) client.write_register.reset_mock() @@ -919,10 +919,10 @@ def test_set_alarm_mode() -> None: meter.alarm_mode = 7 """Test 3: Try set alarm mode at maximum value.""" - mock_result.registers = [4117] + mock_result.registers = [0x1015] client.write_register.return_value = mock_result meter.alarm_mode = 6 - client.write_register.assert_called_once_with(address=4117, value=6, device_id=1) + client.write_register.assert_called_once_with(address=0x1015, value=6, device_id=1) """Test 4: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -939,10 +939,10 @@ def test_set_alarm_delay() -> None: meter = Em511(1, client) """Test 1: Set alarm delay""" - mock_result.registers = [4122] + mock_result.registers = [0x101A] client.write_register.return_value = mock_result meter.alarm_delay = 0 - client.write_register.assert_called_once_with(address=4122, value=0, device_id=1) + client.write_register.assert_called_once_with(address=0x101A, value=0, device_id=1) client.write_register.reset_mock() @@ -951,10 +951,10 @@ def test_set_alarm_delay() -> None: meter.alarm_delay = 3601 """Test 3: Try set alarm delay at maximum value.""" - mock_result.registers = [4122] + mock_result.registers = [0x101A] client.write_register.return_value = mock_result meter.alarm_delay = 3600 - client.write_register.assert_called_once_with(address=4122, value=3600, device_id=1) + client.write_register.assert_called_once_with(address=0x101A, value=3600, device_id=1) """Test 4: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -971,10 +971,10 @@ def test_set_device_id() -> None: meter = Em511(1, client) """Test 1: Set device id""" - mock_result.registers = [8192] + mock_result.registers = [0x2000] client.write_register.return_value = mock_result meter.device_id = 123 - client.write_register.assert_called_once_with(address=8192, value=123, device_id=1) + client.write_register.assert_called_once_with(address=0x2000, value=123, device_id=1) client.write_register.reset_mock() @@ -987,18 +987,18 @@ def test_set_device_id() -> None: meter.device_id = 0 """Test 4: Try set device id at maximum value.""" - mock_result.registers = [8192] + mock_result.registers = [0x2000] client.write_register.return_value = mock_result meter.device_id = 247 - client.write_register.assert_called_once_with(address=8192, value=247, device_id=1) + client.write_register.assert_called_once_with(address=0x2000, value=247, device_id=1) client.write_register.reset_mock() """Test 5: Try set device id at lowest value.""" - mock_result.registers = [8192] + mock_result.registers = [0x2000] client.write_register.return_value = mock_result meter.device_id = 1 - client.write_register.assert_called_once_with(address=8192, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x2000, value=1, device_id=1) """Test 6: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -1015,10 +1015,10 @@ def test_set_baud_rate() -> None: meter = Em511(1, client) """Test 1: Set baud rate""" - mock_result.registers = [8193] + mock_result.registers = [0x2001] client.write_register.return_value = mock_result meter.baud_rate = 2 - client.write_register.assert_called_once_with(address=8193, value=2, device_id=1) + client.write_register.assert_called_once_with(address=0x2001, value=2, device_id=1) client.write_register.reset_mock() @@ -1031,18 +1031,18 @@ def test_set_baud_rate() -> None: meter.baud_rate = 0 """Test 4: Try set baud rate at maximum value.""" - mock_result.registers = [8193] + mock_result.registers = [0x2001] client.write_register.return_value = mock_result meter.baud_rate = 5 - client.write_register.assert_called_once_with(address=8193, value=5, device_id=1) + client.write_register.assert_called_once_with(address=0x2001, value=5, device_id=1) client.write_register.reset_mock() """Test 5: Try set baud rate at lowest value.""" - mock_result.registers = [8193] + mock_result.registers = [0x2001] client.write_register.return_value = mock_result meter.baud_rate = 1 - client.write_register.assert_called_once_with(address=8193, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x2001, value=1, device_id=1) """Test 6: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -1059,10 +1059,10 @@ def test_set_parity() -> None: meter = Em511(1, client) """Test 1: Set parity""" - mock_result.registers = [8194] + mock_result.registers = [0x2002] client.write_register.return_value = mock_result meter.parity = 1 - client.write_register.assert_called_once_with(address=8194, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x2002, value=1, device_id=1) client.write_register.reset_mock() @@ -1075,10 +1075,10 @@ def test_set_parity() -> None: meter.parity = 0 """Test 4: Try set parity at maximum value.""" - mock_result.registers = [8194] + mock_result.registers = [0x2002] client.write_register.return_value = mock_result meter.parity = 2 - client.write_register.assert_called_once_with(address=8194, value=2, device_id=1) + client.write_register.assert_called_once_with(address=0x2002, value=2, device_id=1) client.write_register.reset_mock() @@ -1097,10 +1097,10 @@ def test_set_stop_bit() -> None: meter = Em511(1, client) """Test 1: Set stop bit""" - mock_result.registers = [8195] + mock_result.registers = [0x2003] client.write_register.return_value = mock_result meter.stop_bit = 0 - client.write_register.assert_called_once_with(address=8195, value=0, device_id=1) + client.write_register.assert_called_once_with(address=0x2003, value=0, device_id=1) client.write_register.reset_mock() @@ -1109,10 +1109,10 @@ def test_set_stop_bit() -> None: meter.stop_bit = 2 """Test 3: Try set stop bit at maximum value.""" - mock_result.registers = [8195] + mock_result.registers = [0x2003] client.write_register.return_value = mock_result meter.stop_bit = 1 - client.write_register.assert_called_once_with(address=8195, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x2003, value=1, device_id=1) client.write_register.reset_mock() @@ -1131,10 +1131,10 @@ def test_set_reply_delay() -> None: meter = Em511(1, client) """Test 1: Set reply delay""" - mock_result.registers = [8196] + mock_result.registers = [0x2004] client.write_register.return_value = mock_result meter.reply_delay = 0 - client.write_register.assert_called_once_with(address=8196, value=0, device_id=1) + client.write_register.assert_called_once_with(address=0x2004, value=0, device_id=1) client.write_register.reset_mock() @@ -1143,10 +1143,10 @@ def test_set_reply_delay() -> None: meter.reply_delay = 1001 """Test 3: Try set reply delay at maximum value.""" - mock_result.registers = [8196] + mock_result.registers = [0x2004] client.write_register.return_value = mock_result meter.reply_delay = 1000 - client.write_register.assert_called_once_with(address=8196, value=1000, device_id=1) + client.write_register.assert_called_once_with(address=0x2004, value=1000, device_id=1) """Test 4: Should raise exception due to failed writing to single register.""" mock_result.isError.return_value = True @@ -1163,10 +1163,10 @@ def test_reset_tot_energy_and_run_hour_counter() -> None: meter = Em511(1, client) """Test 1: Reset""" - mock_result.registers = [16387] + mock_result.registers = [0x4003] client.write_register.return_value = mock_result meter.reset_tot_energy_and_run_hour_counter() - client.write_register.assert_called_once_with(address=16387, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x4003, value=1, device_id=1) client.write_register.reset_mock() @@ -1185,10 +1185,10 @@ def test_reset_partial_energy_and_hour_counter() -> None: meter = Em511(1, client) """Test 1: Reset""" - mock_result.registers = [16388] + mock_result.registers = [0x4004] client.write_register.return_value = mock_result meter.reset_partial_energy_and_hour_counter() - client.write_register.assert_called_once_with(address=16388, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x4004, value=1, device_id=1) client.write_register.reset_mock() @@ -1207,10 +1207,10 @@ def test_reset_dmd_and_dmd_max() -> None: meter = Em511(1, client) """Test 1: Reset""" - mock_result.registers = [16389] + mock_result.registers = [0x4005] client.write_register.return_value = mock_result meter.reset_dmd_and_dmd_max() - client.write_register.assert_called_once_with(address=16389, value=1, device_id=1) + client.write_register.assert_called_once_with(address=0x4005, value=1, device_id=1) client.write_register.reset_mock() @@ -1232,8 +1232,8 @@ def test_reset_to_factory_settings() -> None: """Test 1: Both writes occur""" meter.reset_to_factory_settings() expected_calls = [ - call(address=16416, value=0x0A0A, device_id=1), - call(address=16416, value=0xC1A0, device_id=1), + call(address=0x4020, value=0x0A0A, device_id=1), + call(address=0x4020, value=0xC1A0, device_id=1), ] assert client.write_register.call_args_list == expected_calls assert client.write_register.call_count == 2